diff --git a/.coveragerc b/.coveragerc index 7c0421c384a..58b3ff7bbf6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -30,11 +30,13 @@ omit = homeassistant/components/browser.py homeassistant/components/camera/* + homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py + homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tplink.py homeassistant/components/discovery.py @@ -56,11 +58,13 @@ omit = homeassistant/components/notify/syslog.py homeassistant/components/notify/xmpp.py homeassistant/components/sensor/bitcoin.py + homeassistant/components/sensor/dht.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rfxtrx.py + homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py diff --git a/.travis.yml b/.travis.yml index 7af8ce86dcd..65e417fffb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python python: - "3.4" install: - - pip install -r requirements.txt + - pip install -r requirements_all.txt - pip install flake8 pylint coveralls script: - flake8 homeassistant --exclude bower_components,external diff --git a/Dockerfile b/Dockerfile index 323b4d154cc..8ce295ae6aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ MAINTAINER Paulus Schoutsen VOLUME /config +RUN pip3 install --no-cache-dir -r requirements_all.txt + #RUN apt-get update && \ # apt-get install -y cython3 libudev-dev && \ # apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 2514b35587f..6d0884967f7 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -4,120 +4,60 @@ from __future__ import print_function import sys import os import argparse -import subprocess -import importlib -DEPENDENCIES = ['requests>=2.0', 'pyyaml>=3.11', 'pytz>=2015.2'] -IS_VIRTUAL = (getattr(sys, 'base_prefix', sys.prefix) != sys.prefix or - hasattr(sys, 'real_prefix')) - - -def validate_python(): - """ Validate we're running the right Python version. """ - major, minor = sys.version_info[:2] - - if major < 3 or (major == 3 and minor < 4): - print("Home Assistant requires atleast Python 3.4") - sys.exit() - - -def ensure_pip(): - """ Validate pip is installed so we can install packages on demand. """ - if importlib.find_loader('pip') is None: - print("Your Python installation did not bundle 'pip'") - print("Home Assistant requires 'pip' to be installed.") - print("Please install pip: " - "https://pip.pypa.io/en/latest/installing.html") - sys.exit() - - -# Copy of homeassistant.util.package because we can't import yet -def install_package(package): - """Install a package on PyPi. Accepts pip compatible package strings. - Return boolean if install successfull.""" - args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] - if not IS_VIRTUAL: - args.append('--user') - try: - return 0 == subprocess.call(args) - except subprocess.SubprocessError: - return False - - -def validate_dependencies(): - """ Validate all dependencies that HA uses. """ - ensure_pip() - - print("Validating dependencies...") - import_fail = False - - for requirement in DEPENDENCIES: - if not install_package(requirement): - import_fail = True - print('Fatal Error: Unable to install dependency', requirement) - - if import_fail: - print(("Install dependencies by running: " - "python3 -m pip install -r requirements.txt")) - sys.exit() - - -def ensure_path_and_load_bootstrap(): - """ Ensure sys load path is correct and load Home Assistant bootstrap. """ - try: - from homeassistant import bootstrap - - except ImportError: - # This is to add support to load Home Assistant using - # `python3 homeassistant` instead of `python3 -m homeassistant` - - # Insert the parent directory of this file into the module search path - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - - from homeassistant import bootstrap - - return bootstrap - - -def validate_git_submodules(): - """ Validate the git submodules are cloned. """ - try: - # pylint: disable=no-name-in-module, unused-variable - from homeassistant.external.noop import WORKING # noqa - except ImportError: - print("Repository submodules have not been initialized") - print("Please run: git submodule update --init --recursive") - sys.exit() +from homeassistant import bootstrap +import homeassistant.config as config_util +from homeassistant.const import EVENT_HOMEASSISTANT_START def ensure_config_path(config_dir): - """ Gets the path to the configuration file. - Creates one if it not exists. """ + """ Validates configuration directory. """ + + lib_dir = os.path.join(config_dir, 'lib') # Test if configuration directory exists if not os.path.isdir(config_dir): - print(('Fatal Error: Unable to find specified configuration ' - 'directory {} ').format(config_dir)) - sys.exit() + if config_dir != config_util.get_default_config_dir(): + print(('Fatal Error: Specified configuration directory does ' + 'not exist {} ').format(config_dir)) + sys.exit(1) - import homeassistant.config as config_util + try: + os.mkdir(config_dir) + except OSError: + print(('Fatal Error: Unable to create default configuration ' + 'directory {} ').format(config_dir)) + sys.exit(1) + # Test if library directory exists + if not os.path.isdir(lib_dir): + try: + os.mkdir(lib_dir) + except OSError: + print(('Fatal Error: Unable to create library ' + 'directory {} ').format(lib_dir)) + sys.exit(1) + + +def ensure_config_file(config_dir): + """ Ensure configuration file exists. """ config_path = config_util.ensure_config_exists(config_dir) if config_path is None: print('Error getting configuration path') - sys.exit() + sys.exit(1) return config_path def get_arguments(): """ Get parsed passed in arguments. """ - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + description="Home Assistant: Observe, Control, Automate.") parser.add_argument( '-c', '--config', metavar='path_to_config_dir', - default="config", + default=config_util.get_default_config_dir(), help="Directory that contains the Home Assistant configuration") parser.add_argument( '--demo-mode', @@ -133,34 +73,21 @@ def get_arguments(): def main(): """ Starts Home Assistant. """ - validate_python() - validate_dependencies() - - # Windows needs this to pick up new modules - importlib.invalidate_caches() - - bootstrap = ensure_path_and_load_bootstrap() - - validate_git_submodules() - args = get_arguments() config_dir = os.path.join(os.getcwd(), args.config) - config_path = ensure_config_path(config_dir) + ensure_config_path(config_dir) if args.demo_mode: - from homeassistant.components import frontend, demo - hass = bootstrap.from_config_dict({ - frontend.DOMAIN: {}, - demo.DOMAIN: {} - }) + 'frontend': {}, + 'demo': {} + }, config_dir=config_dir) else: - hass = bootstrap.from_config_file(config_path) + config_file = ensure_config_file(config_dir) + hass = bootstrap.from_config_file(config_file) if args.open_ui: - from homeassistant.const import EVENT_HOMEASSISTANT_START - def open_browser(event): """ Open the webinterface in a browser. """ if hass.config.api is not None: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e5f6d2b9672..03cb762d56e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -10,6 +10,7 @@ start by calling homeassistant.start_home_assistant(bus) """ import os +import sys import logging from collections import defaultdict @@ -61,13 +62,13 @@ def setup_component(hass, domain, config=None): return True -def _handle_requirements(component, name): +def _handle_requirements(hass, component, name): """ Installs requirements for component. """ if not hasattr(component, 'REQUIREMENTS'): return True for req in component.REQUIREMENTS: - if not pkg_util.install_package(req): + if not pkg_util.install_package(req, target=hass.config.path('lib')): _LOGGER.error('Not initializing %s because could not install ' 'dependency %s', name, req) return False @@ -88,7 +89,7 @@ def _setup_component(hass, domain, config): domain, ", ".join(missing_deps)) return False - if not _handle_requirements(component, domain): + if not _handle_requirements(hass, component, domain): return False try: @@ -138,14 +139,19 @@ def prepare_setup_platform(hass, config, domain, platform_name): component) return None - if not _handle_requirements(platform, platform_path): + if not _handle_requirements(hass, platform, platform_path): return None return platform +def mount_local_lib_path(config_dir): + """ Add local library to Python Path """ + sys.path.insert(0, os.path.join(config_dir, 'lib')) + + # pylint: disable=too-many-branches, too-many-statements -def from_config_dict(config, hass=None): +def from_config_dict(config, hass=None, config_dir=None, enable_log=True): """ Tries to configure Home Assistant from a config dict. @@ -153,10 +159,15 @@ def from_config_dict(config, hass=None): """ if hass is None: hass = core.HomeAssistant() + if config_dir is not None: + config_dir = os.path.abspath(config_dir) + hass.config.config_dir = config_dir + mount_local_lib_path(config_dir) process_ha_core_config(hass, config.get(core.DOMAIN, {})) - enable_logging(hass) + if enable_log: + enable_logging(hass) _ensure_loader_prepared(hass) @@ -195,11 +206,15 @@ def from_config_file(config_path, hass=None): hass = core.HomeAssistant() # Set config dir to directory holding config file - hass.config.config_dir = os.path.abspath(os.path.dirname(config_path)) + config_dir = os.path.abspath(os.path.dirname(config_path)) + hass.config.config_dir = config_dir + mount_local_lib_path(config_dir) + + enable_logging(hass) config_dict = config_util.load_config_file(config_path) - return from_config_dict(config_dict, hass) + return from_config_dict(config_dict, hass, enable_log=False) def enable_logging(hass): diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 54d16bcca37..8dcb158dea4 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -62,8 +62,12 @@ def _get_action(hass, config): service_data = {} if CONF_SERVICE_ENTITY_ID in config: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID].split(",") + try: + service_data[ATTR_ENTITY_ID] = \ + config[CONF_SERVICE_ENTITY_ID].split(",") + except AttributeError: + service_data[ATTR_ENTITY_ID] = \ + config[CONF_SERVICE_ENTITY_ID] hass.services.call(domain, service, service_data) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 17d20571f62..71621502878 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -10,11 +10,11 @@ import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.loader as loader from homeassistant.const import ( - CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID) + CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) DOMAIN = "demo" -DEPENDENCIES = [] +DEPENDENCIES = ['introduction', 'conversation'] COMPONENTS_WITH_DEMO_PLATFORM = [ 'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify'] @@ -48,8 +48,11 @@ def setup(hass, config): # Setup room groups lights = hass.states.entity_ids('light') switches = hass.states.entity_ids('switch') - group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0]]) - group.setup_group(hass, 'bedroom', [lights[2], switches[1]]) + media_players = sorted(hass.states.entity_ids('media_player')) + group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0], + media_players[1]]) + group.setup_group(hass, 'bedroom', [lights[2], switches[1], + media_players[0]]) # Setup IP Camera bootstrap.setup_component( @@ -102,10 +105,10 @@ def setup(hass, config): # Setup fake device tracker hass.states.set("device_tracker.paulus", "home", {ATTR_ENTITY_PICTURE: - "http://graph.facebook.com/297400035/picture"}) + "http://graph.facebook.com/297400035/picture", + ATTR_FRIENDLY_NAME: 'Paulus'}) hass.states.set("device_tracker.anne_therese", "not_home", - {ATTR_ENTITY_PICTURE: - "http://graph.facebook.com/621994601/picture"}) + {ATTR_FRIENDLY_NAME: 'Anne Therese'}) hass.states.set("group.all_devices", "home", { diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 452480b12c9..099c23973f0 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -12,6 +12,7 @@ from datetime import timedelta from homeassistant.loader import get_component from homeassistant.helpers import validate_config +from homeassistant.helpers.entity import _OVERWRITE import homeassistant.util as util import homeassistant.util.dt as dt_util @@ -66,14 +67,15 @@ def setup(hass, config): 'device_tracker.{}'.format(tracker_type)) if tracker_implementation is None: - _LOGGER.error("Unknown device_tracker type specified.") + _LOGGER.error("Unknown device_tracker type specified: %s.", + tracker_type) return False device_scanner = tracker_implementation.get_scanner(hass, config) if device_scanner is None: - _LOGGER.error("Failed to initialize device scanner for %s", + _LOGGER.error("Failed to initialize device scanner: %s", tracker_type) return False @@ -161,9 +163,12 @@ class DeviceTracker(object): state = STATE_HOME if is_home else STATE_NOT_HOME + # overwrite properties that have been set in the config file + attr = dict(dev_info['state_attr']) + attr.update(_OVERWRITE.get(dev_info['entity_id'], {})) + self.hass.states.set( - dev_info['entity_id'], state, - dev_info['state_attr']) + dev_info['entity_id'], state, attr) def update_devices(self, now): """ Update device states based on the found devices. """ diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py new file mode 100644 index 00000000000..06956475ba0 --- /dev/null +++ b/homeassistant/components/device_tracker/actiontec.py @@ -0,0 +1,149 @@ +""" +homeassistant.components.device_tracker.actiontec +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning an Actiontec MI424WR +(Verizon FIOS) router for device presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the Actiontec tracker you will need to add something like the +following to your config/configuration.yaml + +device_tracker: + platform: actiontec + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +_LEASES_REGEX = re.compile( + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + + r'\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a DD-WRT scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ActiontecDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ActiontecDeviceScanner(object): + """ This class queries a an actiontec router + for connected devices. Adapted from DD-WRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_actiontec_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['ip'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the Actiontec MI424WR router is up + to date. Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + # _LOGGER.info("Checking ARP") + data = self.get_actiontec_data() + if not data: + return False + active_clients = [client for client in data.values()] + self.last_results = active_clients + return True + + def get_actiontec_data(self): + """ Retrieve data from Actiontec MI424WR and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'Username: ') + telnet.write((self.username + '\n').encode('ascii')) + telnet.read_until(b'Password: ') + telnet.write((self.password + '\n').encode('ascii')) + prompt = telnet.read_until( + b'Wireless Broadband Router> ').split(b'\n')[-1] + telnet.write('firewall mac_cache_dump\n'.encode('ascii')) + telnet.write('\n'.encode('ascii')) + telnet.read_until(prompt) + leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] + telnet.write('exit\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return None + + devices = {} + for lease in leases_result: + match = _LEASES_REGEX.search(lease.decode('utf-8')) + if match is not None: + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper() + } + return devices diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 3fe11f99fe6..346fbb37d37 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -35,7 +35,6 @@ from datetime import timedelta import threading from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import validate_config from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN @@ -43,20 +42,21 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear>=0.1'] +REQUIREMENTS = ['pynetgear==0.3'] def get_scanner(hass, config): """ Validates config and returns a Netgear scanner. """ - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): + info = config[DOMAIN] + host = info.get(CONF_HOST) + username = info.get(CONF_USERNAME) + password = info.get(CONF_PASSWORD) + + if password is not None and host is None: + _LOGGER.warning('Found username or password but no host') return None - info = config[DOMAIN] - - scanner = NetgearDeviceScanner( - info[CONF_HOST], info[CONF_USERNAME], info[CONF_PASSWORD]) + scanner = NetgearDeviceScanner(host, username, password) return scanner if scanner.success_init else None @@ -68,16 +68,24 @@ class NetgearDeviceScanner(object): import pynetgear self.last_results = [] - - self._api = pynetgear.Netgear(host, username, password) self.lock = threading.Lock() + if host is None: + print("BIER") + self._api = pynetgear.Netgear() + elif username is None: + self._api = pynetgear.Netgear(password, host) + else: + self._api = pynetgear.Netgear(password, host, username) + _LOGGER.info("Logging in") - self.success_init = self._api.login() + results = self._api.get_attached_devices() + + self.success_init = results is not None if self.success_init: - self._update_info() + self.last_results = results else: _LOGGER.error("Failed to Login") diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 8876ed3d488..ee1650594ee 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -26,8 +26,12 @@ from collections import namedtuple import subprocess import re -from libnmap.process import NmapProcess -from libnmap.parser import NmapParser, NmapParserException +try: + from libnmap.process import NmapProcess + from libnmap.parser import NmapParser, NmapParserException + LIB_LOADED = True +except ImportError: + LIB_LOADED = False import homeassistant.util.dt as dt_util from homeassistant.const import CONF_HOSTS @@ -43,7 +47,7 @@ _LOGGER = logging.getLogger(__name__) # interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" -REQUIREMENTS = ['python-libnmap>=0.6.2'] +REQUIREMENTS = ['python-libnmap==0.6.1'] def get_scanner(hass, config): @@ -52,6 +56,10 @@ def get_scanner(hass, config): _LOGGER): return None + if not LIB_LOADED: + _LOGGER.error("Error while importing dependency python-libnmap.") + return False + scanner = NmapDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py new file mode 100644 index 00000000000..ffe1a7f64c2 --- /dev/null +++ b/homeassistant/components/device_tracker/thomson.py @@ -0,0 +1,157 @@ +""" +homeassistant.components.device_tracker.thomson +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a THOMSON router for device +presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the THOMSON tracker you will need to add something like the following +to your config/configuration.yaml + +device_tracker: + platform: thomson + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + +_LOGGER = logging.getLogger(__name__) + +_DEVICES_REGEX = re.compile( + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))\s+' + + r'(?P([^\s]+))') + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a THOMSON scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ThomsonDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ThomsonDeviceScanner(object): + """ This class queries a router running THOMSON firmware + for connected devices. Adapted from ASUSWRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_thomson_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device + or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['host'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the THOMSON router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + data = self.get_thomson_data() + if not data: + return False + + # flag C stands for CONNECTED + active_clients = [client for client in data.values() if + client['status'].find('C') != -1] + self.last_results = active_clients + return True + + def get_thomson_data(self): + """ Retrieve data from THOMSON and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'Username : ') + telnet.write((self.username + '\r\n').encode('ascii')) + telnet.read_until(b'Password : ') + telnet.write((self.password + '\r\n').encode('ascii')) + telnet.read_until(b'=>') + telnet.write(('hostmgr list\r\n').encode('ascii')) + devices_result = telnet.read_until(b'=>').split(b'\r\n') + telnet.write('exit\r\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return + + devices = {} + for device in devices_result: + match = _DEVICES_REGEX.search(device.decode('utf-8')) + if match: + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper(), + 'host': match.group('host'), + 'status': match.group('status') + } + return devices diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 0aa7312bfd7..c21249fbc60 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -19,7 +19,7 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['netdisco>=0.1'] +REQUIREMENTS = ['netdisco==0.3'] SCAN_INTERVAL = 300 # seconds @@ -28,11 +28,13 @@ SCAN_INTERVAL = 300 # seconds SERVICE_WEMO = 'belkin_wemo' SERVICE_HUE = 'philips_hue' SERVICE_CAST = 'google_cast' +SERVICE_NETGEAR = 'netgear_router' SERVICE_HANDLERS = { SERVICE_WEMO: "switch", SERVICE_CAST: "media_player", SERVICE_HUE: "light", + SERVICE_NETGEAR: 'device_tracker', } @@ -77,6 +79,13 @@ def setup(hass, config): if not component: return + # Hack - fix when device_tracker supports discovery + if service == SERVICE_NETGEAR: + bootstrap.setup_component(hass, component, { + 'device_tracker': {'platform': 'netgear'} + }) + return + # This component cannot be setup. if not bootstrap.setup_component(hass, component, config): return diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index f84c8653b31..556fe4e67c8 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -5,24 +5,46 @@ Home Assistant - - - - - - + + + - - -

Initializing Home Assistant

- - - + + + +
+ +

Initializing

+
+ + + diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index d78c936465d..f4a1ad184e7 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "541d58d78257af6cf484b923f3b39c1e" +VERSION = "e9060d58fc9034468cfefa9794026d0c" diff --git a/homeassistant/components/frontend/www_static/__init__.py b/homeassistant/components/frontend/www_static/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 629a9e17aa9..e1b3b84dfc3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -516,49 +516,6 @@ if(n>=0){var s=e.getKey(t);return this.splice("selected",n,1),this.unlinkPaths(" left: 0; }; - } \ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 576c04efb49..a97750b5dd8 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 576c04efb49a8a5f7f35734458ffc93f874dd68d +Subproject commit a97750b5dd887af42030e01bfe50bc3c60183514 diff --git a/homeassistant/components/frontend/www_static/images/__init__.py b/homeassistant/components/frontend/www_static/images/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/frontend/www_static/splash.png b/homeassistant/components/frontend/www_static/splash.png new file mode 100644 index 00000000000..582140a2bc3 Binary files /dev/null and b/homeassistant/components/frontend/www_static/splash.png differ diff --git a/homeassistant/components/frontend/www_static/version.py b/homeassistant/components/frontend/www_static/version.py deleted file mode 100644 index 0f9641f054b..00000000000 --- a/homeassistant/components/frontend/www_static/version.py +++ /dev/null @@ -1,2 +0,0 @@ -""" DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "" diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py new file mode 100644 index 00000000000..b84a02d5fa5 --- /dev/null +++ b/homeassistant/components/introduction.py @@ -0,0 +1,41 @@ +""" +homeassistant.components.introduction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Component that will help guide the user taking its first steps. +""" +import logging + +DOMAIN = 'introduction' +DEPENDENCIES = [] + + +def setup(hass, config=None): + """ Setup the introduction component. """ + log = logging.getLogger(__name__) + log.info(""" + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Hello, and welcome to Home Assistant! + + We'll hope that we can make all your dreams come true. + + Here are some resources to get started: + + - Configuring Home Assistant: + https://home-assistant.io/getting-started/configuration.html + + - Available components: + https://home-assistant.io/components/ + + - Chat room: + https://gitter.im/balloob/home-assistant + + This message is generated by the introduction component. You can + disable it in configuration.yaml. + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + """) + + return True diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 5cdfbe1b277..63c7b6c4af6 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -21,7 +21,7 @@ from homeassistant.const import ( DOMAIN = "isy994" DEPENDENCIES = [] -REQUIREMENTS = ['PyISY>=1.0.5'] +REQUIREMENTS = ['PyISY==1.0.5'] DISCOVER_LIGHTS = "isy994.lights" DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" @@ -156,6 +156,12 @@ class ISYDeviceABC(ToggleEntity): attr = {ATTR_FRIENDLY_NAME: self.name} for name, prop in self._attrs.items(): attr[name] = getattr(self, prop) + attr = self._attr_filter(attr) + return attr + + def _attr_filter(self, attr): + """ Placeholder for attribute filters. """ + # pylint: disable=no-self-use return attr @property diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 5359791087e..3629fce31bf 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -14,7 +14,7 @@ from homeassistant.const import ( DOMAIN = "keyboard" DEPENDENCIES = [] -REQUIREMENTS = ['pyuserinput>=0.1.9'] +REQUIREMENTS = ['pyuserinput==0.1.9'] def volume_up(hass): diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index f012d160b7f..b438d7b92b1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT, EFFECT_COLORLOOP) -REQUIREMENTS = ['phue>=0.8'] +REQUIREMENTS = ['phue==0.8'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return if discovery_info is not None: - host = urlparse(discovery_info).hostname + host = urlparse(discovery_info[1]).hostname else: host = config.get(CONF_HOST, None) diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index b231fe3e441..5b62120ee98 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -38,3 +38,9 @@ class ISYLightDevice(ISYDeviceABC): _attrs = {ATTR_BRIGHTNESS: 'value'} _onattrs = [ATTR_BRIGHTNESS] _states = [STATE_ON, STATE_OFF] + + def _attr_filter(self, attr): + """ Filter brightness out of entity while off. """ + if ATTR_BRIGHTNESS in attr and not self.is_on: + del attr[ATTR_BRIGHTNESS] + return attr diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index b3e0858ffe2..8fdb525d4e0 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -34,7 +34,7 @@ from homeassistant.components.light import (Light, ATTR_BRIGHTNESS, from homeassistant.util.color import color_RGB_to_xy _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['ledcontroller>=1.0.7'] +REQUIREMENTS = ['ledcontroller==1.0.7'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 9132604b294..8068d20bb74 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -9,7 +9,7 @@ from homeassistant.components.light import Light, ATTR_BRIGHTNESS from homeassistant.const import ATTR_FRIENDLY_NAME import tellcore.constants as tellcore_constants -REQUIREMENTS = ['tellcore-py>=1.0.4'] +REQUIREMENTS = ['tellcore-py==1.0.4'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index e8c8eb7a224..4b5af0c3250 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -9,8 +9,8 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/master.zip' - '#pywink>=0.1'] +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 17ae9e6cd33..d19e4166c1c 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -8,14 +8,9 @@ WARNING: This platform is currently not working due to a changed Cast API """ import logging -try: - import pychromecast -except ImportError: - pychromecast = None - from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF, - STATE_UNKNOWN) + STATE_UNKNOWN, CONF_HOST) from homeassistant.components.media_player import ( MediaPlayerDevice, @@ -24,7 +19,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) -REQUIREMENTS = ['pychromecast>=0.6.9'] +REQUIREMENTS = ['pychromecast==0.6.10'] CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -32,21 +27,23 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE KNOWN_HOSTS = [] +# pylint: disable=invalid-name +cast = None + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the cast platform. """ - global pychromecast # pylint: disable=invalid-name - if pychromecast is None: - import pychromecast as pychromecast_ - pychromecast = pychromecast_ + global cast + import pychromecast + cast = pychromecast logger = logging.getLogger(__name__) # import CEC IGNORE attributes ignore_cec = config.get(CONF_IGNORE_CEC, []) if isinstance(ignore_cec, list): - pychromecast.IGNORE_CEC += ignore_cec + cast.IGNORE_CEC += ignore_cec else: logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC) @@ -55,9 +52,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info and discovery_info[0] not in KNOWN_HOSTS: hosts = [discovery_info[0]] + elif CONF_HOST in config: + hosts = [config[CONF_HOST]] + else: hosts = (host_port[0] for host_port - in pychromecast.discover_chromecasts() + in cast.discover_chromecasts() if host_port[0] not in KNOWN_HOSTS) casts = [] @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for host in hosts: try: casts.append(CastDevice(host)) - except pychromecast.ChromecastConnectionError: + except cast.ChromecastConnectionError: pass else: KNOWN_HOSTS.append(host) @@ -80,7 +80,7 @@ class CastDevice(MediaPlayerDevice): def __init__(self, host): import pychromecast.controllers.youtube as youtube - self.cast = pychromecast.Chromecast(host) + self.cast = cast.Chromecast(host) self.youtube = youtube.YouTubeController() self.cast.register_handler(self.youtube) @@ -226,7 +226,7 @@ class CastDevice(MediaPlayerDevice): self.cast.quit_app() self.cast.play_media( - CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) + CAST_SPLASH, cast.STREAM_TYPE_BUFFERED) def turn_off(self): """ Turns Chromecast off. """ diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 4b24f8694ed..dfc2f64a2a8 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -48,7 +48,7 @@ except ImportError: jsonrpc_requests = None _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['jsonrpc-requests>=0.1'] +REQUIREMENTS = ['jsonrpc-requests==0.1'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 0239173f7cc..aca2413d3e4 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -48,7 +48,7 @@ from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-mpd2>=0.5.4'] +REQUIREMENTS = ['python-mpd2==0.5.4'] SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 0bd3b23c2f9..e6c3f1cfcee 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -38,8 +38,8 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, DOMAIN = "modbus" DEPENDENCIES = [] -REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/python3.zip' - '#pymodbus>=1.2.0'] +REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/' + + 'd7fc4f1cc975631e0a9011390e8017f64b612661.zip'] # Type of network MEDIUM = "type" diff --git a/homeassistant/components/mqtt.py b/homeassistant/components/mqtt.py index aa1a3167029..474b5ebb53e 100644 --- a/homeassistant/components/mqtt.py +++ b/homeassistant/components/mqtt.py @@ -46,7 +46,7 @@ The keep alive in seconds for this client. Default is 60. import logging import socket -from homeassistant.core import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError import homeassistant.util as util from homeassistant.helpers import validate_config from homeassistant.const import ( @@ -66,7 +66,7 @@ SERVICE_PUBLISH = 'publish' EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' DEPENDENCIES = [] -REQUIREMENTS = ['paho-mqtt>=1.1'] +REQUIREMENTS = ['paho-mqtt==1.1'] CONF_BROKER = 'broker' CONF_PORT = 'port' diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index ecd15aeb8e2..ee53159d5e6 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -4,12 +4,13 @@ homeassistant.components.notify Provides functionality to notify people. """ +from functools import partial import logging from homeassistant.loader import get_component -from homeassistant.helpers import validate_config +from homeassistant.helpers import config_per_platform -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_NAME DOMAIN = "notify" DEPENDENCIES = [] @@ -33,42 +34,45 @@ def send_message(hass, message): def setup(hass, config): """ Sets up notify services. """ + success = False - if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER): - return False + for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER): + # get platform + notify_implementation = get_component( + 'notify.{}'.format(platform)) - platform = config[DOMAIN].get(CONF_PLATFORM) + if notify_implementation is None: + _LOGGER.error("Unknown notification service specified.") + continue - notify_implementation = get_component( - 'notify.{}'.format(platform)) + # create platform service + notify_service = notify_implementation.get_service( + hass, {DOMAIN: p_config}) - if notify_implementation is None: - _LOGGER.error("Unknown notification service specified.") + if notify_service is None: + _LOGGER.error("Failed to initialize notification service %s", + platform) + continue - return False + # create service handler + def notify_message(notify_service, call): + """ Handle sending notification message service calls. """ + message = call.data.get(ATTR_MESSAGE) - notify_service = notify_implementation.get_service(hass, config) + if message is None: + return - if notify_service is None: - _LOGGER.error("Failed to initialize notification service %s", - platform) + title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - return False + notify_service.send_message(message, title=title) - def notify_message(call): - """ Handle sending notification message service calls. """ - message = call.data.get(ATTR_MESSAGE) + # register service + service_call_handler = partial(notify_message, notify_service) + service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY) + hass.services.register(DOMAIN, service_notify, service_call_handler) + success = True - if message is None: - return - - title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - notify_service.send_message(message, title=title) - - hass.services.register(DOMAIN, SERVICE_NOTIFY, notify_message) - - return True + return success # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 5e322cfc3b5..58462954d2e 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -28,7 +28,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_API_KEY _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pushbullet.py>=0.7.1'] +REQUIREMENTS = ['pushbullet.py==0.7.1'] def get_service(hass, config): diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 1bc5e9ac9a3..0df035a4a6e 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -42,7 +42,7 @@ from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) from homeassistant.const import CONF_API_KEY -REQUIREMENTS = ['python-pushover>=0.2'] +REQUIREMENTS = ['python-pushover==0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 859b5b0388a..d604cffb754 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -32,7 +32,7 @@ from homeassistant.components.notify import ( DOMAIN, BaseNotificationService) from homeassistant.const import CONF_API_KEY -REQUIREMENTS = ['slacker>=0.6.8'] +REQUIREMENTS = ['slacker==0.6.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 25099921f45..81268c734b7 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -1,7 +1,6 @@ """ homeassistant.components.notify.xmpp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Jabber (XMPP) notification service. Configuration: @@ -29,7 +28,6 @@ The password for your given Jabber account. recipient *Required The Jabber ID (JID) that will receive the messages. - """ import logging @@ -47,7 +45,7 @@ from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) -REQUIREMENTS = ['sleekxmpp>=1.3.1'] +REQUIREMENTS = ['sleekxmpp==1.3.1', 'dnspython3==1.12.0'] def get_service(hass, config): diff --git a/homeassistant/components/scheduler/__init__.py b/homeassistant/components/scheduler/__init__.py index e69ba6627a7..1a67636da3d 100644 --- a/homeassistant/components/scheduler/__init__.py +++ b/homeassistant/components/scheduler/__init__.py @@ -131,7 +131,7 @@ class ServiceEventListener(EventListener): def execute(self, hass): """ Call the service. """ data = {ATTR_ENTITY_ID: self.my_schedule.entity_ids} - hass.call_service(self.domain, self.service, data) + hass.services.call(self.domain, self.service, data) # Reschedule for next day self.schedule(hass) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index e0ecbab6db5..b30886448ad 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -71,7 +71,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['blockchain>=1.1.2'] +REQUIREMENTS = ['blockchain==1.1.2'] _LOGGER = logging.getLogger(__name__) OPTION_TYPES = { 'wallet': ['Wallet balance', 'BTC'], diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 37e2555136a..218860290b0 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -15,6 +15,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ DemoSensor('Outside Temperature', 15.6, TEMP_CELCIUS, 12), DemoSensor('Outside Humidity', 54, '%', None), + DemoSensor('Alarm back', 'Armed', None, None), ]) diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py new file mode 100644 index 00000000000..7949a7a44fa --- /dev/null +++ b/homeassistant/components/sensor/dht.py @@ -0,0 +1,164 @@ +""" +homeassistant.components.sensor.dht +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adafruit DHT temperature and humidity sensor. +You need a Python3 compatible version of the Adafruit_Python_DHT library +(e.g. https://github.com/mala-zaba/Adafruit_Python_DHT, +also see requirements.txt). +As this requires access to the GPIO, you will need to run home-assistant +as root. + +Configuration: + +To use the Adafruit DHT sensor you will need to +add something like the following to your config/configuration.yaml: + +sensor: + platform: dht + sensor: DHT22 + pin: 23 + monitored_conditions: + - temperature + - humidity + +Variables: + +sensor +*Required +The sensor type, DHT11, DHT22 or AM2302 + +pin +*Required +The pin the sensor is connected to, something like +'P8_11' for Beaglebone, '23' for Raspberry Pi + +monitored_conditions +*Optional +Conditions to monitor. Available conditions are temperature and humidity. +""" +import logging +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.helpers.entity import Entity + +# update this requirement to upstream as soon as it supports python3 +REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/' + + '4101340de8d2457dd194bca1e8d11cbfc237e919.zip'] +_LOGGER = logging.getLogger(__name__) +SENSOR_TYPES = { + 'temperature': ['Temperature', ''], + 'humidity': ['Humidity', '%'] +} +# Return cached results if last scan was less then this time ago +# DHT11 is able to deliver data once per second, DHT22 once every two +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the DHT sensor. """ + + try: + import Adafruit_DHT + + except ImportError: + _LOGGER.exception( + "Unable to import Adafruit_DHT. " + "Did you maybe not install the 'Adafruit_DHT' package?") + + return False + + SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit + unit = hass.config.temperature_unit + available_sensors = { + "DHT11": Adafruit_DHT.DHT11, + "DHT22": Adafruit_DHT.DHT22, + "AM2302": Adafruit_DHT.AM2302 + } + sensor = available_sensors[config['sensor']] + + pin = config['pin'] + + if not sensor or not pin: + _LOGGER.error( + "Config error " + "Please check your settings for DHT, sensor not supported.") + return None + + data = DHTClient(Adafruit_DHT, sensor, pin) + dev = [] + try: + for variable in config['monitored_conditions']: + if variable not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable) + else: + dev.append(DHTSensor(data, variable, unit)) + except KeyError: + pass + + add_devices(dev) + + +# pylint: disable=too-few-public-methods +class DHTSensor(Entity): + + """ Implements an DHT sensor. """ + + def __init__(self, dht_client, sensor_type, temp_unit): + self.client_name = 'DHT sensor' + self._name = SENSOR_TYPES[sensor_type][0] + self.dht_client = dht_client + self.temp_unit = temp_unit + self.type = sensor_type + self._state = None + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.update() + + @property + def name(self): + return '{} {}'.format(self.client_name, self._name) + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement + + def update(self): + """ Gets the latest data from the DHT and updates the states. """ + + self.dht_client.update() + data = self.dht_client.data + + if self.type == 'temperature': + self._state = round(data['temperature'], 1) + if self.temp_unit == TEMP_FAHRENHEIT: + self._state = round(data['temperature'] * 1.8 + 32, 1) + elif self.type == 'humidity': + self._state = round(data['humidity'], 1) + + +class DHTClient(object): + + """ Gets the latest data from the DHT sensor. """ + + def __init__(self, adafruit_dht, sensor, pin): + self.adafruit_dht = adafruit_dht + self.sensor = sensor + self.pin = pin + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data the DHT sensor. """ + humidity, temperature = self.adafruit_dht.read_retry(self.sensor, + self.pin) + if temperature: + self.data['temperature'] = temperature + if humidity: + self.data['humidity'] = humidity diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index a9783104cd8..b56432ab89b 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -49,7 +49,7 @@ Details for the API : https://developer.forecast.io/docs/v2 import logging from datetime import timedelta -REQUIREMENTS = ['python-forecastio>=1.3.3'] +REQUIREMENTS = ['python-forecastio==1.3.3'] try: import forecastio diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py new file mode 100644 index 00000000000..d5dc192e450 --- /dev/null +++ b/homeassistant/components/sensor/mqtt.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +""" +homeassistant.components.sensor.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT sensor. + +This generic sensor implementation uses the MQTT message payload +as the sensor value. If messages in this state_topic are published +with RETAIN flag, the sensor will receive an instant update with +last known value. Otherwise, the initial state will be undefined. + +sensor: + platform: mqtt + name: "MQTT Sensor" + state_topic: "home/bedroom/temperature" + unit_of_measurement: "ÂșC" + +Variables: + +name +*Optional +The name of the sensor. Default is 'MQTT Sensor'. + +state_topic +*Required +The MQTT topic subscribed to receive sensor values. + +unit_of_measurement +*Optional +Defines the units of measurement of the sensor, if any. + +""" + +import logging +from homeassistant.helpers.entity import Entity +import homeassistant.components.mqtt as mqtt + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "MQTT Sensor" + +DEPENDENCIES = ['mqtt'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add MQTT Sensor """ + + if config.get('state_topic') is None: + _LOGGER.error("Missing required variable: state_topic") + return False + + add_devices_callback([MqttSensor( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic'), + config.get('unit_of_measurement'))]) + + +class MqttSensor(Entity): + """ Represents a sensor that can be updated using MQTT """ + def __init__(self, hass, name, state_topic, unit_of_measurement): + self._state = "-" + self._hass = hass + self._name = name + self._state_topic = state_topic + self._unit_of_measurement = unit_of_measurement + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + self._state = payload + self.update_ha_state() + + mqtt.subscribe(hass, self._state_topic, message_received) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the sensor """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit this state is expressed in. """ + return self._unit_of_measurement + + @property + def state(self): + """ Returns the state of the entity. """ + return self._state diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index a626858db31..994b110a585 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -36,8 +36,8 @@ ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/master.zip' - '#egg=pymysensors-0.1'] +REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' + + '35b87d880147a34107da0d40cb815d75e6cb4af7.zip'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index f4635cd13ca..537fc9f59b5 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -48,7 +48,7 @@ from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyowm>=2.2.1'] +REQUIREMENTS = ['pyowm==2.2.1'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'weather': ['Condition', ''], diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index ffc688804ef..8e5a1ad3dca 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -26,8 +26,8 @@ from collections import OrderedDict from homeassistant.const import (TEMP_CELCIUS) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip' - '#RFXtrx>=0.15'] +REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/' + + 'ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip'] DATA_TYPES = OrderedDict([ ('Temperature', TEMP_CELCIUS), diff --git a/homeassistant/components/sensor/rpi_gpio.py b/homeassistant/components/sensor/rpi_gpio.py new file mode 100644 index 00000000000..f973b24a301 --- /dev/null +++ b/homeassistant/components/sensor/rpi_gpio.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +homeassistant.components.sensor.rpi_gpio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a binary state sensor using RPi GPIO. +Note: To use RPi GPIO, Home Assistant must be run as root. + +sensor: + platform: rpi_gpio + pull_mode: "UP" + value_high: "Active" + value_low: "Inactive" + ports: + 11: PIR Office + 12: PIR Bedroom + +Variables: + +pull_mode +*Optional +The internal pull to use (UP or DOWN). Default is UP. + +value_high +*Optional +The value of the sensor when the port is HIGH. Default is "HIGH". + +value_low +*Optional +The value of the sensor when the port is LOW. Default is "LOW". + +bouncetime +*Optional +The time in milliseconds for port debouncing. Default is 50ms. + +ports +*Required +An array specifying the GPIO ports to use and the name to use in the frontend. + +""" + +import logging +from homeassistant.helpers.entity import Entity +try: + import RPi.GPIO as GPIO +except ImportError: + GPIO = None +from homeassistant.const import (DEVICE_DEFAULT_NAME, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) + +DEFAULT_PULL_MODE = "UP" +DEFAULT_VALUE_HIGH = "HIGH" +DEFAULT_VALUE_LOW = "LOW" +DEFAULT_BOUNCETIME = 50 + +REQUIREMENTS = ['RPi.GPIO==0.5.11'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Raspberry PI GPIO ports. """ + if GPIO is None: + _LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.') + return + # pylint: disable=no-member + GPIO.setmode(GPIO.BCM) + + sensors = [] + pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE) + value_high = config.get('value_high', DEFAULT_VALUE_HIGH) + value_low = config.get('value_low', DEFAULT_VALUE_LOW) + bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME) + ports = config.get('ports') + for port_num, port_name in ports.items(): + sensors.append(RPiGPIOSensor( + port_name, port_num, pull_mode, + value_high, value_low, bouncetime)) + add_devices(sensors) + + def cleanup_gpio(event): + """ Stuff to do before stop home assistant. """ + # pylint: disable=no-member + GPIO.cleanup() + + def prepare_gpio(event): + """ Stuff to do when home assistant starts. """ + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class RPiGPIOSensor(Entity): + """ Sets up the Raspberry PI GPIO ports. """ + def __init__(self, port_name, port_num, pull_mode, + value_high, value_low, bouncetime): + # pylint: disable=no-member + self._name = port_name or DEVICE_DEFAULT_NAME + self._port = port_num + self._pull = GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP + self._vhigh = value_high + self._vlow = value_low + self._bouncetime = bouncetime + GPIO.setup(self._port, GPIO.IN, pull_up_down=self._pull) + self._state = self._vhigh if GPIO.input(self._port) else self._vlow + + def edge_callback(channel): + """ port changed state """ + # pylint: disable=no-member + self._state = self._vhigh if GPIO.input(channel) else self._vlow + self.update_ha_state() + + GPIO.add_event_detect( + self._port, + GPIO.BOTH, + callback=edge_callback, + bouncetime=self._bouncetime) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the sensor """ + return self._name + + @property + def state(self): + """ Returns the state of the entity. """ + return self._state diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 1d1bdb1f3b5..b473cc27283 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -66,7 +66,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_ON, STATE_OFF -REQUIREMENTS = ['psutil>=3.0.0'] +REQUIREMENTS = ['psutil==3.0.0'] SENSOR_TYPES = { 'disk_use_percent': ['Disk Use', '%'], 'disk_use': ['Disk Use', 'GiB'], diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 7d024333023..e93c6e4c97f 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -35,7 +35,7 @@ import homeassistant.util as util DatatypeDescription = namedtuple("DatatypeDescription", ['name', 'unit']) -REQUIREMENTS = ['tellcore-py>=1.0.4'] +REQUIREMENTS = ['tellcore-py==1.0.4'] # pylint: disable=unused-argument diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index e443e81b93f..8579a922661 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -18,7 +18,8 @@ from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/rkabadi/temper-python/archive/master.zip'] +REQUIREMENTS = ['https://github.com/rkabadi/temper-python/archive/' + + '3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip'] # pylint: disable=unused-argument diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index b9ed3ea4e9f..587f5131d9d 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -67,7 +67,7 @@ from transmissionrpc.error import TransmissionError import logging -REQUIREMENTS = ['transmissionrpc>=0.11'] +REQUIREMENTS = ['transmissionrpc==0.11'] SENSOR_TYPES = { 'current_status': ['Status', ''], 'download_speed': ['Down Speed', 'MB/s'], diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 4056bbd7733..0b3d33cea24 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -8,8 +8,8 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED -REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/master.zip' - '#pywink>=0.1'] +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 507c4a2b63b..802eddb4a3a 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -31,7 +31,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.scheduler import ServiceEventListener DEPENDENCIES = [] -REQUIREMENTS = ['astral>=0.8.1'] +REQUIREMENTS = ['astral==0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 17fe6d61735..200c5746e27 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -44,7 +44,8 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD,\ DEFAULT_USERNAME = 'admin' DEFAULT_PASSWORD = '1234' DEVICE_DEFAULT_NAME = 'Edimax Smart Plug' -REQUIREMENTS = ['https://github.com/rkabadi/pyedimax/archive/master.zip'] +REQUIREMENTS = ['https://github.com/rkabadi/pyedimax/archive/' + + '365301ce3ff26129a7910c501ead09ea625f3700.zip'] # setup logger _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 5ab084319fc..6ab82df482a 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -49,7 +49,7 @@ except ImportError: hikvision.api = None _LOGGING = logging.getLogger(__name__) -REQUIREMENTS = ['hikvision>=0.4'] +REQUIREMENTS = ['hikvision==0.4'] # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py new file mode 100644 index 00000000000..c6ebdaa2ad6 --- /dev/null +++ b/homeassistant/components/switch/mqtt.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +""" +homeassistant.components.switch.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a MQTT switch. + +In an ideal scenario, the MQTT device will have a state topic to publish +state changes. If these messages are published with RETAIN flag, the MQTT +switch will receive an instant state update after subscription and will +start with correct state. Otherwise, the initial state of the switch will +be false/off. + +When a state topic is not available, the switch will work in optimistic mode. +In this mode, the switch will immediately change state after every command. +Otherwise, the switch will wait for state confirmation from device +(message from state_topic). + +Optimistic mode can be forced, even if state topic is available. +Try to enable it, if experiencing incorrect switch operation. + + +Configuration: + +switch: + platform: mqtt + name: "Bedroom Switch" + state_topic: "home/bedroom/switch1" + command_topic: "home/bedroom/switch1/set" + payload_on: "ON" + payload_off: "OFF" + optimistic: false + +Variables: + +name +*Optional +The name of the switch. Default is 'MQTT Switch'. + +state_topic +*Optional +The MQTT topic subscribed to receive state updates. +If not specified, optimistic mode will be forced. + +command_topic +*Required +The MQTT topic to publish commands to change the switch state. + +payload_on +*Optional +The payload that represents enabled state. Default is "ON". + +payload_off +*Optional +The payload that represents disabled state. Default is "OFF". + +optimistic +*Optional +Flag that defines if switch works in optimistic mode. Default is false. + +""" + +import logging +import homeassistant.components.mqtt as mqtt +from homeassistant.components.switch import SwitchDevice + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "MQTT Switch" +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_OPTIMISTIC = False + +DEPENDENCIES = ['mqtt'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Add MQTT Switch """ + + if config.get('command_topic') is None: + _LOGGER.error("Missing required variable: command_topic") + return False + + add_devices_callback([MqttSwitch( + hass, + config.get('name', DEFAULT_NAME), + config.get('state_topic'), + config.get('command_topic'), + config.get('payload_on', DEFAULT_PAYLOAD_ON), + config.get('payload_off', DEFAULT_PAYLOAD_OFF), + config.get('optimistic', DEFAULT_OPTIMISTIC))]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class MqttSwitch(SwitchDevice): + """ Represents a switch that can be togggled using MQTT """ + def __init__(self, hass, name, state_topic, command_topic, + payload_on, payload_off, optimistic): + self._state = False + self._hass = hass + self._name = name + self._state_topic = state_topic + self._command_topic = command_topic + self._payload_on = payload_on + self._payload_off = payload_off + self._optimistic = optimistic + + def message_received(topic, payload, qos): + """ A new MQTT message has been received. """ + if payload == self._payload_on: + self._state = True + self.update_ha_state() + elif payload == self._payload_off: + self._state = False + self.update_ha_state() + + if self._state_topic is None: + # force optimistic mode + self._optimistic = True + else: + # subscribe the state_topic + mqtt.subscribe(hass, self._state_topic, message_received) + + @property + def should_poll(self): + """ No polling needed """ + return False + + @property + def name(self): + """ The name of the switch """ + return self._name + + @property + def is_on(self): + """ True if device is on. """ + return self._state + + def turn_on(self, **kwargs): + """ Turn the device on. """ + mqtt.publish(self.hass, self._command_topic, self._payload_on) + if self._optimistic: + # optimistically assume that switch has changed state + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + mqtt.publish(self.hass, self._command_topic, self._payload_off) + if self._optimistic: + # optimistically assume that switch has changed state + self._state = False + self.update_ha_state() diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index 127d02a121d..4afa38aa80a 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -2,21 +2,28 @@ homeassistant.components.switch.rpi_gpio ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows to control the GPIO pins of a Raspberry Pi. +Note: To use RPi GPIO, Home Assistant must be run as root. Configuration: switch: platform: rpi_gpio + invert_logic: false ports: 11: Fan Office 12: Light Desk Variables: +invert_logic +*Optional +If true, inverts the output logic to ACTIVE LOW. Default is false (ACTIVE HIGH) + ports *Required An array specifying the GPIO ports to use and the name to use in the frontend. """ + import logging try: import RPi.GPIO as GPIO @@ -27,7 +34,9 @@ from homeassistant.const import (DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['RPi.GPIO>=0.5.11'] +DEFAULT_INVERT_LOGIC = False + +REQUIREMENTS = ['RPi.GPIO==0.5.11'] _LOGGER = logging.getLogger(__name__) @@ -37,11 +46,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if GPIO is None: _LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.') return + # pylint: disable=no-member + GPIO.setmode(GPIO.BCM) switches = [] + invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC) ports = config.get('ports') for port_num, port_name in ports.items(): - switches.append(RPiGPIOSwitch(port_name, port_num)) + switches.append(RPiGPIOSwitch(port_name, port_num, invert_logic)) add_devices(switches) def cleanup_gpio(event): @@ -59,10 +71,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RPiGPIOSwitch(ToggleEntity): """ Represents a port that can be toggled using Raspberry Pi GPIO. """ - def __init__(self, name, gpio): + def __init__(self, name, gpio, invert_logic): self._name = name or DEVICE_DEFAULT_NAME - self._state = False self._gpio = gpio + self._active_state = not invert_logic + self._state = not self._active_state # pylint: disable=no-member GPIO.setup(gpio, GPIO.OUT) @@ -83,13 +96,13 @@ class RPiGPIOSwitch(ToggleEntity): def turn_on(self, **kwargs): """ Turn the device on. """ - if self._switch(True): + if self._switch(self._active_state): self._state = True self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ - if self._switch(False): + if self._switch(not self._active_state): self._state = False self.update_ha_state() diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 4fde4babf9e..230151382e7 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -19,7 +19,7 @@ import tellcore.constants as tellcore_constants SINGAL_REPETITIONS = 1 -REQUIREMENTS = ['tellcore-py>=1.0.4'] +REQUIREMENTS = ['tellcore-py==1.0.4'] # pylint: disable=unused-argument diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index 7575951f53b..d5cf716c770 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -48,7 +48,7 @@ from transmissionrpc.error import TransmissionError import logging _LOGGING = logging.getLogger(__name__) -REQUIREMENTS = ['transmissionrpc>=0.11'] +REQUIREMENTS = ['transmissionrpc==0.11'] # pylint: disable=unused-argument diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 3020f638dd3..2d6e25b296b 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.switch import SwitchDevice -REQUIREMENTS = ['pywemo>=0.1'] +REQUIREMENTS = ['pywemo==0.2'] # pylint: disable=unused-argument @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo.discovery as discovery if discovery_info is not None: - device = discovery.device_from_description(discovery_info) + device = discovery.device_from_description(discovery_info[2]) if device: add_devices_callback([WemoSwitch(device)]) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 556a40b181f..c9fb045d9c0 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -9,8 +9,8 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/master.zip' - '#pywink>=0.1'] +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 40e392709f2..bbc0979e38c 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -10,6 +10,7 @@ from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util from homeassistant.helpers.entity import Entity +from homeassistant.helpers.temperature import convert from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, TEMP_CELCIUS) @@ -86,7 +87,9 @@ def setup(hass, config): return for thermostat in target_thermostats: - thermostat.set_temperature(temperature) + thermostat.set_temperature(convert( + temperature, hass.config.temperature_unit, + thermostat.unit_of_measurement)) for thermostat in target_thermostats: thermostat.update_ha_state(True) @@ -118,9 +121,20 @@ class ThermostatDevice(Entity): @property def state_attributes(self): """ Returns optional state attributes. """ + + thermostat_unit = self.unit_of_measurement + user_unit = self.hass.config.temperature_unit + data = { - ATTR_CURRENT_TEMPERATURE: self.hass.config.temperature( - self.current_temperature, self.unit_of_measurement)[0] + ATTR_CURRENT_TEMPERATURE: round(convert(self.current_temperature, + thermostat_unit, + user_unit), 1), + ATTR_MIN_TEMP: round(convert(self.min_temp, + thermostat_unit, + user_unit), 0), + ATTR_MAX_TEMP: round(convert(self.max_temp, + thermostat_unit, + user_unit), 0) } is_away = self.is_away_mode_on @@ -133,11 +147,13 @@ class ThermostatDevice(Entity): if device_attr is not None: data.update(device_attr) - data[ATTR_MIN_TEMP] = self.min_temp - data[ATTR_MAX_TEMP] = self.max_temp - return data + @property + def unit_of_measurement(self): + """ Unit of measurement this thermostat expresses itself in. """ + return NotImplementedError + @property def current_temperature(self): """ Returns the current temperature. """ @@ -171,9 +187,9 @@ class ThermostatDevice(Entity): @property def min_temp(self): """ Return minimum temperature. """ - return self.hass.config.temperature(7, TEMP_CELCIUS)[0] + return convert(7, TEMP_CELCIUS, self.unit_of_measurement) @property def max_temp(self): """ Return maxmum temperature. """ - return self.hass.config.temperature(35, TEMP_CELCIUS)[0] + return convert(35, TEMP_CELCIUS, self.unit_of_measurement) diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index b2e48b96bcd..1de729b590d 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -6,7 +6,7 @@ import logging from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) -REQUIREMENTS = ['python-nest>=2.3.1'] +REQUIREMENTS = ['python-nest==2.4.0'] # pylint: disable=unused-argument @@ -50,11 +50,19 @@ class NestThermostat(ThermostatDevice): @property def name(self): """ Returns the name of the nest, if any. """ - return self.device.name + location = self.device.where + name = self.device.name + if location is None: + return name + else: + if name == '': + return location.capitalize() + else: + return location.capitalize() + '(' + name + ')' @property def unit_of_measurement(self): - """ Returns the unit of measurement. """ + """ Unit of measurement this thermostat expresses itself in. """ return TEMP_CELCIUS @property @@ -109,6 +117,24 @@ class NestThermostat(ThermostatDevice): """ Turns away off. """ self.structure.away = False + @property + def min_temp(self): + """ Identifies min_temp in Nest API or defaults if not available. """ + temp = self.device.away_temperature.low + if temp is None: + return super().min_temp + else: + return temp + + @property + def max_temp(self): + """ Identifies mxn_temp in Nest API or defaults if not available. """ + temp = self.device.away_temperature.high + if temp is None: + return super().max_temp + else: + return temp + def update(self): """ Python-nest has its own mechanism for staying up to date. """ pass diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index f084ce9874c..d716c8c46ad 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -61,7 +61,8 @@ DISCOVER_SWITCHES = 'verisure.switches' DEPENDENCIES = [] REQUIREMENTS = [ - 'https://github.com/persandstrom/python-verisure/archive/master.zip' + 'https://github.com/persandstrom/python-verisure/archive/' + + '9873c4527f01b1ba1f72ae60f7f35854390d59be.zip' ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index d56a244b84c..eb2beac508a 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -16,8 +16,8 @@ from homeassistant.const import ( DOMAIN = "wink" DEPENDENCIES = [] -REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/master.zip' - '#pywink>=0.1'] +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' + + 'c2b700e8ca866159566ecf5e644d9c297f69f257.zip'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index ce189a242b4..ef7e7308959 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -12,7 +12,7 @@ from homeassistant.const import ( DOMAIN = "zwave" DEPENDENCIES = [] -REQUIREMENTS = ['pydispatcher>=2.0.5'] +REQUIREMENTS = ['pydispatcher==2.0.5'] CONF_USB_STICK_PATH = "usb_path" DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" diff --git a/homeassistant/config.py b/homeassistant/config.py index 6ae40e9e7c7..78da7f2a0d1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ Module to help with parsing and generating configuration files. import logging import os -from homeassistant.core import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) @@ -16,6 +16,7 @@ import homeassistant.util.location as loc_util _LOGGER = logging.getLogger(__name__) YAML_CONFIG_FILE = 'configuration.yaml' +CONFIG_DIR_NAME = '.homeassistant' DEFAULT_CONFIG = ( # Tuples (attribute, default, auto detect property, description) @@ -28,8 +29,22 @@ DEFAULT_CONFIG = ( (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), ) -DEFAULT_COMPONENTS = ( - 'discovery', 'frontend', 'conversation', 'history', 'logbook', 'sun') +DEFAULT_COMPONENTS = { + 'introduction': 'Show links to resources in log and frontend', + 'frontend': 'Enables the frontend', + 'discovery': 'Discover some devices automatically', + 'conversation': 'Allows you to issue voice commands from the frontend', + 'history': 'Enables support for tracking state changes over time.', + 'logbook': 'View all events in a logbook', + 'sun': 'Track the sun', +} + + +def get_default_config_dir(): + """ Put together the default configuration directory based on OS. """ + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + return os.path.join(data_dir, CONFIG_DIR_NAME) def ensure_config_exists(config_dir, detect_location=True): @@ -39,7 +54,8 @@ def ensure_config_exists(config_dir, detect_location=True): config_path = find_config_file(config_dir) if config_path is None: - _LOGGER.info("Unable to find configuration. Creating default one") + print("Unable to find configuration. Creating default one at", + config_dir) config_path = create_default_config(config_dir, detect_location) return config_path @@ -78,15 +94,14 @@ def create_default_config(config_dir, detect_location=True): config_file.write("\n") - for component in DEFAULT_COMPONENTS: + for component, description in DEFAULT_COMPONENTS.items(): + config_file.write("# {}\n".format(description)) config_file.write("{}:\n\n".format(component)) return config_path except IOError: - _LOGGER.exception( - 'Unable to write default configuration file %s', config_path) - + print('Unable to create default configuration file', config_path) return None diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d58dbb01d2..a3b9cc8d396 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,7 @@ """ Constants used by Home Assistant components. """ + +__version__ = "0.7.0" + # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' diff --git a/homeassistant/core.py b/homeassistant/core.py index 76b4b38f3fc..c04e9a9ab63 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -21,9 +21,12 @@ from homeassistant.const import ( EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) +from homeassistant.exceptions import ( + HomeAssistantError, InvalidEntityFormatError) import homeassistant.util as util import homeassistant.util.dt as date_util import homeassistant.helpers.temperature as temp_helper +from homeassistant.config import get_default_config_dir DOMAIN = "homeassistant" @@ -660,7 +663,7 @@ class Config(object): self.api = None # Directory that holds the configuration - self.config_dir = os.path.join(os.getcwd(), 'config') + self.config_dir = get_default_config_dir() def path(self, *path): """ Returns path to the file within the config dir. """ @@ -695,21 +698,6 @@ class Config(object): } -class HomeAssistantError(Exception): - """ General Home Assistant exception occured. """ - pass - - -class InvalidEntityFormatError(HomeAssistantError): - """ When an invalid formatted entity is encountered. """ - pass - - -class NoEntitySpecifiedError(HomeAssistantError): - """ When no entity is specified. """ - pass - - def create_timer(hass, interval=TIMER_INTERVAL): """ Creates a timer. Timer will start on HOMEASSISTANT_START. """ # We want to be able to fire every time a minute starts (seconds=0). diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py new file mode 100644 index 00000000000..bd32d356670 --- /dev/null +++ b/homeassistant/exceptions.py @@ -0,0 +1,16 @@ +""" Exceptions used by Home Assistant """ + + +class HomeAssistantError(Exception): + """ General Home Assistant exception occured. """ + pass + + +class InvalidEntityFormatError(HomeAssistantError): + """ When an invalid formatted entity is encountered. """ + pass + + +class NoEntitySpecifiedError(HomeAssistantError): + """ When no entity is specified. """ + pass diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 0ca63856c27..b29379049d3 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,7 +7,7 @@ Provides ABC for entities in HA. from collections import defaultdict -from homeassistant.core import NoEntitySpecifiedError +from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, ATTR_HIDDEN, diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b0eb8d287db..7b755214252 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -166,9 +166,10 @@ def load_order_components(components): key=lambda order: 'group' in order): load_order.update(comp_load_order) - # Push recorder to first place in load order - if 'recorder' in load_order: - load_order.promote('recorder') + # Push some to first place in load order + for comp in ('recorder', 'introduction'): + if comp in load_order: + load_order.promote(comp) return load_order diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 2488f0a9c46..2193ede86e7 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -18,6 +18,7 @@ import urllib.parse import requests import homeassistant.core as ha +from homeassistant.exceptions import HomeAssistantError import homeassistant.bootstrap as bootstrap from homeassistant.const import ( @@ -84,12 +85,12 @@ class API(object): except requests.exceptions.ConnectionError: _LOGGER.exception("Error connecting to server") - raise ha.HomeAssistantError("Error connecting to server") + raise HomeAssistantError("Error connecting to server") except requests.exceptions.Timeout: error = "Timeout when talking to {}".format(self.host) _LOGGER.exception(error) - raise ha.HomeAssistantError(error) + raise HomeAssistantError(error) def __repr__(self): return "API({}, {}, {})".format( @@ -102,7 +103,7 @@ class HomeAssistant(ha.HomeAssistant): def __init__(self, remote_api, local_api=None): if not remote_api.validate_api(): - raise ha.HomeAssistantError( + raise HomeAssistantError( "Remote API at {}:{} not valid: {}".format( remote_api.host, remote_api.port, remote_api.status)) @@ -121,7 +122,7 @@ class HomeAssistant(ha.HomeAssistant): # Ensure a local API exists to connect with remote if self.config.api is None: if not bootstrap.setup_component(self, 'api'): - raise ha.HomeAssistantError( + raise HomeAssistantError( 'Unable to setup local API to receive events') ha.create_timer(self) @@ -132,7 +133,7 @@ class HomeAssistant(ha.HomeAssistant): # Setup that events from remote_api get forwarded to local_api # Do this after we fire START, otherwise HTTP is not started if not connect_remote_events(self.remote_api, self.config.api): - raise ha.HomeAssistantError(( + raise HomeAssistantError(( 'Could not setup event forwarding from api {} to ' 'local api {}').format(self.remote_api, self.config.api)) @@ -293,7 +294,7 @@ def validate_api(api): else: return APIStatus.UNKNOWN - except ha.HomeAssistantError: + except HomeAssistantError: return APIStatus.CANNOT_CONNECT @@ -318,7 +319,7 @@ def connect_remote_events(from_api, to_api): return False - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error setting up event forwarding") return False @@ -342,7 +343,7 @@ def disconnect_remote_events(from_api, to_api): return False - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error removing an event forwarder") return False @@ -354,7 +355,7 @@ def get_event_listeners(api): return req.json() if req.status_code == 200 else {} - except (ha.HomeAssistantError, ValueError): + except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json _LOGGER.exception("Unexpected result retrieving event listeners") @@ -371,7 +372,7 @@ def fire_event(api, event_type, data=None): _LOGGER.error("Error firing event: %d - %d", req.status_code, req.text) - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error firing event") @@ -387,7 +388,7 @@ def get_state(api, entity_id): return ha.State.from_dict(req.json()) \ if req.status_code == 200 else None - except (ha.HomeAssistantError, ValueError): + except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json _LOGGER.exception("Error fetching state") @@ -404,7 +405,7 @@ def get_states(api): return [ha.State.from_dict(item) for item in req.json()] - except (ha.HomeAssistantError, ValueError, AttributeError): + except (HomeAssistantError, ValueError, AttributeError): # ValueError if req.json() can't parse the json _LOGGER.exception("Error fetching states") @@ -434,7 +435,7 @@ def set_state(api, entity_id, new_state, attributes=None): else: return True - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error setting state") return False @@ -457,7 +458,7 @@ def get_services(api): return req.json() if req.status_code == 200 else {} - except (ha.HomeAssistantError, ValueError): + except (HomeAssistantError, ValueError): # ValueError if req.json() can't parse the json _LOGGER.exception("Got unexpected services result") @@ -475,5 +476,5 @@ def call_service(api, domain, service, service_data=None): _LOGGER.error("Error calling service: %d - %s", req.status_code, req.text) - except ha.HomeAssistantError: + except HomeAssistantError: _LOGGER.exception("Error calling service") diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index d220a5a7e61..802e3834b90 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,22 +1,18 @@ """Helpers to install PyPi packages.""" +import os import subprocess import sys -from . import environment as env -# If we are not in a virtual environment, install in user space -INSTALL_USER = not env.is_virtual() - - -def install_package(package, upgrade=False, user=INSTALL_USER): +def install_package(package, upgrade=False, target=None): """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successfull.""" # Not using 'import pip; pip.main([])' because it breaks the logger args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] if upgrade: args.append('--upgrade') - if user: - args.append('--user') + if target: + args += ['--target', os.path.abspath(target)] try: return 0 == subprocess.call(args) except subprocess.SubprocessError: diff --git a/pylintrc b/pylintrc index 54b1f80cdc5..888fb50ee0f 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,5 @@ [MASTER] -ignore=external +ignore=external,setup.py reports=no # Reasons disabled: diff --git a/requirements.txt b/requirements.txt index 24027be2d3b..1b7d2396971 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,114 +1,4 @@ -# Required for Home Assistant core -requests>=2.0 -pyyaml>=3.11 -pytz>=2015.2 - -# Optional, needed for specific components - -# Sun (sun) -astral>=0.8.1 - -# Philips Hue library (lights.hue) -phue>=0.8 - -# Limitlessled/Easybulb/Milight library (lights.limitlessled) -ledcontroller>=1.0.7 - -# Chromecast bindings (media_player.cast) -pychromecast>=0.6.9 - -# Keyboard (keyboard) -pyuserinput>=0.1.9 - -# Tellstick bindings (*.tellstick) -tellcore-py>=1.0.4 - -# Nmap bindings (device_tracker.nmap) -python-libnmap>=0.6.2 - -# PushBullet bindings (notify.pushbullet) -pushbullet.py>=0.7.1 - -# Nest Thermostat bindings (thermostat.nest) -python-nest>=2.3.1 - -# Z-Wave (*.zwave) -pydispatcher>=2.0.5 - -# ISY994 bindings (*.isy994) -PyISY>=1.0.5 - -# PSutil (sensor.systemmonitor) -psutil>=3.0.0 - -# Pushover bindings (notify.pushover) -python-pushover>=0.2 - -# Transmission Torrent Client (*.transmission) -transmissionrpc>=0.11 - -# OpenWeatherMap Web API (sensor.openweathermap) -pyowm>=2.2.1 - -# XMPP Bindings (notify.xmpp) -sleekxmpp>=1.3.1 - -# Blockchain (sensor.bitcoin) -blockchain>=1.1.2 - -# MPD Bindings (media_player.mpd) -python-mpd2>=0.5.4 - -# Hikvision (switch.hikvisioncam) -hikvision>=0.4 - -# console log coloring -colorlog>=2.6.0 - -# JSON-RPC interface (media_player.kodi) -jsonrpc-requests>=0.1 - -# Forecast.io Bindings (sensor.forecast) -python-forecastio>=1.3.3 - -# Firmata Bindings (*.arduino) -PyMata==2.07a - -#Rfxtrx sensor -https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip - -# Mysensors -https://github.com/theolind/pymysensors/archive/master.zip#egg=pymysensors-0.1 - -# Netgear (device_tracker.netgear) -pynetgear>=0.1 - -# Netdisco (discovery) -netdisco>=0.1 - -# Wemo (switch.wemo) -pywemo>=0.1 - -# Wink (*.wink) -https://github.com/balloob/python-wink/archive/master.zip#pywink>=0.1 - -# Slack notifier -slacker>=0.6.8 - -# Temper sensors -https://github.com/rkabadi/temper-python/archive/master.zip - -# PyEdimax -https://github.com/rkabadi/pyedimax/archive/master.zip - -# RPI-GPIO platform -RPi.GPIO >=0.5.11 - -# PAHO MQTT Binding (protocol.mqtt) -paho-mqtt>=1.1 - -# PyModbus (modbus) -https://github.com/bashwork/pymodbus/archive/python3.zip#pymodbus>=1.2.0 - -# Verisure -https://github.com/persandstrom/python-verisure/archive/master.zip +requests>=2,<3 +pyyaml>=3.11,<4 +pytz>=2015.4 +pip>=7.0.0 diff --git a/requirements_all.txt b/requirements_all.txt new file mode 100644 index 00000000000..a900846e30d --- /dev/null +++ b/requirements_all.txt @@ -0,0 +1,121 @@ +# Required for Home Assistant core +requests>=2,<3 +pyyaml>=3.11,<4 +pytz>=2015.4 +pip>=7.0.0 + +# Optional, needed for specific components + +# Sun (sun) +astral==0.8.1 + +# Philips Hue library (lights.hue) +phue==0.8 + +# Limitlessled/Easybulb/Milight library (lights.limitlessled) +ledcontroller==1.0.7 + +# Chromecast bindings (media_player.cast) +pychromecast==0.6.10 + +# Keyboard (keyboard) +pyuserinput==0.1.9 + +# Tellstick bindings (*.tellstick) +tellcore-py==1.0.4 + +# Nmap bindings (device_tracker.nmap) +python-libnmap==0.6.3 + +# PushBullet bindings (notify.pushbullet) +pushbullet.py==0.7.1 + +# Nest Thermostat bindings (thermostat.nest) +python-nest==2.4.0 + +# Z-Wave (*.zwave) +pydispatcher==2.0.5 + +# ISY994 bindings (*.isy994) +PyISY==1.0.5 + +# PSutil (sensor.systemmonitor) +psutil==3.0.0 + +# Pushover bindings (notify.pushover) +python-pushover==0.2 + +# Transmission Torrent Client (*.transmission) +transmissionrpc==0.11 + +# OpenWeatherMap Web API (sensor.openweathermap) +pyowm==2.2.1 + +# XMPP Bindings (notify.xmpp) +sleekxmpp==1.3.1 +dnspython3==1.12.0 + +# Blockchain (sensor.bitcoin) +blockchain==1.1.2 + +# MPD Bindings (media_player.mpd) +python-mpd2==0.5.4 + +# Hikvision (switch.hikvisioncam) +hikvision==0.4 + +# console log coloring +colorlog==2.6.0 + +# JSON-RPC interface (media_player.kodi) +jsonrpc-requests==0.1 + +# Forecast.io Bindings (sensor.forecast) +python-forecastio==1.3.3 + +# Firmata Bindings (*.arduino) +PyMata==2.07a + +# Rfxtrx sensor (sensor.rfxtrx) +https://github.com/Danielhiversen/pyRFXtrx/archive/ec7a1aaddf8270db6e5da1c13d58c1547effd7cf.zip + +# Mysensors +https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75e6cb4af7.zip + +# Netgear (device_tracker.netgear) +pynetgear==0.3 + +# Netdisco (discovery) +netdisco==0.3 + +# Wemo (switch.wemo) +pywemo==0.2 + +# Wink (*.wink) +https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip + +# Slack notifier (notify.slack) +slacker==0.6.8 + +# Temper sensors (sensor.temper) +https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip + +# PyEdimax +https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip + +# RPI-GPIO platform (*.rpi_gpio) +# Uncomment for Raspberry Pi +# RPi.GPIO ==0.5.11 + +# Adafruit temperature/humidity sensor +# uncomment on a Raspberry Pi / Beaglebone +# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip + +# PAHO MQTT Binding (mqtt) +paho-mqtt==1.1 + +# PyModbus (modbus) +https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip + +# Verisure (verisure) +https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip diff --git a/scripts/homeassistant.daemon b/scripts/homeassistant.daemon index bef4cd90f4a..4dd6b37a9c5 100755 --- a/scripts/homeassistant.daemon +++ b/scripts/homeassistant.daemon @@ -79,7 +79,7 @@ case "$1" in uninstall) uninstall ;; - retart) + restart) stop start ;; diff --git a/scripts/update b/scripts/update index 7f2b59147bd..afeacbb1235 100755 --- a/scripts/update +++ b/scripts/update @@ -3,6 +3,4 @@ if [ ${PWD##*/} == "scripts" ]; then cd .. fi -git pull --recurse-submodules=yes -git submodule update --init --recursive -python3 -m pip install -r requirements.txt +git pull diff --git a/setup.py b/setup.py new file mode 100755 index 00000000000..610a7398735 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +import os +import re +from setuptools import setup, find_packages + +PACKAGE_NAME = 'homeassistant' +HERE = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(HERE, PACKAGE_NAME, 'const.py')) as fp: + VERSION = re.search("__version__ = ['\"]([^']+)['\"]\n", fp.read()).group(1) +DOWNLOAD_URL = \ + 'https://github.com/balloob/home-assistant/tarball/{}'.format(VERSION) + +PACKAGES = find_packages() + \ + ['homeassistant.external', 'homeassistant.external.noop', + 'homeassistant.external.nzbclients', 'homeassistant.external.vera'] + +PACKAGE_DATA = \ + {'homeassistant.components.frontend': ['index.html.template'], + 'homeassistant.components.frontend.www_static': ['*.*'], + 'homeassistant.components.frontend.www_static.images': ['*.*']} + +REQUIRES = \ + [line.strip() for line in open('requirements.txt', 'r')] + +setup( + name=PACKAGE_NAME, + version=VERSION, + license='MIT License', + url='https://home-assistant.io/', + download_url=DOWNLOAD_URL, + author='Paulus Schoutsen', + author_email='paulus@paulusschoutsen.nl', + description='Open-source home automation platform running on Python 3.', + packages=PACKAGES, + include_package_data=True, + package_data=PACKAGE_DATA, + zip_safe=False, + platforms='any', + install_requires=REQUIRES, + keywords=['home', 'automation'], + entry_points={ + 'console_scripts': [ + 'hass = homeassistant.__main__:main' + ] + }, + classifiers=[ + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.4', + 'Topic :: Home Automation' + ] +) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 2af17ea405c..507c37dc20a 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -78,3 +78,18 @@ class TestAutomationEvent(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) self.assertEqual(['hello.world'], self.calls[0].data[ATTR_ENTITY_ID]) + + def test_service_specify_entity_id_list(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_SERVICE_ENTITY_ID: ['hello.world', 'hello.world2'] + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + self.assertEqual(['hello.world', 'hello.world2'], self.calls[0].data[ATTR_ENTITY_ID]) diff --git a/tests/test_core.py b/tests/test_core.py index 6e7b52795b2..1aab679805a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -16,6 +16,8 @@ from datetime import datetime import pytz import homeassistant.core as ha +from homeassistant.exceptions import ( + HomeAssistantError, InvalidEntityFormatError) import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_state_change from homeassistant.const import ( @@ -41,7 +43,7 @@ class TestHomeAssistant(unittest.TestCase): """ Stop down stuff we started. """ try: self.hass.stop() - except ha.HomeAssistantError: + except HomeAssistantError: # Already stopped after the block till stopped test pass @@ -250,7 +252,7 @@ class TestState(unittest.TestCase): def test_init(self): """ Test state.init """ self.assertRaises( - ha.InvalidEntityFormatError, ha.State, + InvalidEntityFormatError, ha.State, 'invalid_entity_format', 'test_state') def test_domain(self): @@ -489,18 +491,24 @@ class TestConfig(unittest.TestCase): def test_config_dir_set_correct(self): """ Test config dir set correct. """ - self.assertEqual(os.path.join(os.getcwd(), "config"), + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + self.assertEqual(os.path.join(data_dir, ".homeassistant"), self.config.config_dir) def test_path_with_file(self): """ Test get_config_path method. """ - self.assertEqual(os.path.join(os.getcwd(), "config", "test.conf"), + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') + self.assertEqual(os.path.join(data_dir, ".homeassistant", "test.conf"), self.config.path("test.conf")) def test_path_with_dir_and_file(self): """ Test get_config_path method. """ + data_dir = os.getenv('APPDATA') if os.name == "nt" \ + else os.path.expanduser('~') self.assertEqual( - os.path.join(os.getcwd(), "config", "dir", "test.conf"), + os.path.join(data_dir, ".homeassistant", "dir", "test.conf"), self.config.path("dir", "test.conf")) def test_temperature_not_convert_if_no_preference(self): diff --git a/tests/test_remote.py b/tests/test_remote.py index 0f45091d598..e5bfd71199f 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -130,9 +130,12 @@ class TestRemoteMethods(unittest.TestCase): def test_set_state(self): """ Test Python API set_state. """ - self.assertTrue(remote.set_state(master_api, 'test.test', 'set_test')) + hass.states.set('test.test', 'set_test') - self.assertEqual('set_test', hass.states.get('test.test').state) + state = hass.states.get('test.test') + + self.assertIsNotNone(state) + self.assertEqual('set_test', state.state) self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test'))