diff --git a/.coveragerc b/.coveragerc index 63763cb9231..9cb5bdc63dd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,8 +4,6 @@ source = homeassistant omit = homeassistant/__main__.py - homeassistant/external/* - # omit pieces of code that rely on external devices being present homeassistant/components/arduino.py homeassistant/components/*/arduino.py @@ -28,14 +26,17 @@ omit = homeassistant/components/zwave.py homeassistant/components/*/zwave.py + homeassistant/components/ifttt.py homeassistant/components/browser.py homeassistant/components/camera/* homeassistant/components/device_tracker/actiontec.py + homeassistant/components/device_tracker/aruba.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 @@ -44,9 +45,12 @@ omit = homeassistant/components/light/hue.py homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/denon.py + homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/sonos.py homeassistant/components/notify/file.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py @@ -56,7 +60,9 @@ omit = homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py homeassistant/components/notify/xmpp.py + homeassistant/components/sensor/arest.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 @@ -69,6 +75,7 @@ omit = homeassistant/components/sensor/temper.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py + homeassistant/components/switch/arest.py homeassistant/components/switch/command_switch.py homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py diff --git a/.gitignore b/.gitignore index 658ad279292..881411c54ea 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ config/custom_components/* !config/custom_components/hello_world.py !config/custom_components/mqtt_example.py +tests/config/home-assistant.log + # Hide sublime text stuff *.sublime-project *.sublime-workspace diff --git a/.gitmodules b/.gitmodules index a627e522d8f..ad28a4e2c8a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,3 @@ -[submodule "homeassistant/external/noop"] - path = homeassistant/external/noop - url = https://github.com/balloob/noop.git -[submodule "homeassistant/external/vera"] - path = homeassistant/external/vera - url = https://github.com/jamespcole/home-assistant-vera-api.git -[submodule "homeassistant/external/nzbclients"] - path = homeassistant/external/nzbclients - url = https://github.com/jamespcole/home-assistant-nzb-clients.git [submodule "homeassistant/components/frontend/www_static/home-assistant-polymer"] path = homeassistant/components/frontend/www_static/home-assistant-polymer url = https://github.com/balloob/home-assistant-polymer.git diff --git a/.travis.yml b/.travis.yml index 7af8ce86dcd..339ed48d424 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,10 @@ 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 + - flake8 homeassistant - pylint homeassistant - coverage run -m unittest discover tests after_success: diff --git a/Dockerfile b/Dockerfile index 323b4d154cc..9554ec552d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,14 @@ MAINTAINER Paulus Schoutsen VOLUME /config +RUN pip3 install --no-cache-dir -r requirements_all.txt + +# For the nmap tracker +RUN apt-get update && \ + apt-get install -y --no-install-recommends nmap net-tools && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Open Z-Wave disabled because broken #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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000000..aae95799ac4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-exclude tests * diff --git a/README.md b/README.md index c9d3045cbb4..6b1b1353392 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ # Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. [Open a demo.](https://home-assistant.io/demo/) +[demo]: https://home-assistant.io/demo/ -Check out [the website](https://home-assistant.io) for installation instructions, tutorials and documentation. +Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. -[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/) +To get started: +```bash +python3 -m pip install homeassistant +hass --open-ui +``` + +Check out [the website](https://home-assistant.io) for [a demo][demo], installation instructions, tutorials and documentation. + +[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)][demo] Examples of devices it can interface it: * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) - * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Efergy](https://efergy.com) plugs, [Edimax](http://www.edimax.com/) switches, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), and [Kodi (XBMC)](http://kodi.tv/) + * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors + * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)) * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) * Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org). * [See full list of supported devices](https://home-assistant.io/components/) @@ -21,26 +29,9 @@ Built home automation on top of your devices: * Turn on the lights when people get home after sun set * Turn on lights slowly during sun set to compensate for less light * Turn off all lights and devices when everybody leaves the house - * Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects + * Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org) The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html). -If you run into issues while using Home Assistant or during development of a component, reach out to the [Home Assistant help section](https://home-assistant.io/help/) how to reach us. - -## Quick-start guide - -Running Home Assistant requires [Python 3.4](https://www.python.org/). Run the following code to get up and running: - -``` -git clone --recursive https://github.com/balloob/home-assistant.git -python3 -m venv home-assistant -cd home-assistant -python3 -m homeassistant --open-ui -``` - -The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/configuration.yaml`. A standard configuration file will be written if none exists. - -If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command. - -Please see [the getting started guide](https://home-assistant.io/getting-started/) on how to further configure Home Assistant. +If you run into issues while using Home Assistant or during development of a component, check the [Home Assistant help section](https://home-assistant.io/help/) how to reach us. diff --git a/config/custom_components/example.py b/config/custom_components/example.py index 5bfb03353e0..ee7f18f437a 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -12,7 +12,7 @@ Example component to target an entity_id to: Configuration: To use the Example custom component you will need to add the following to -your config/configuration.yaml +your configuration.yaml file. example: target: TARGET_ENTITY diff --git a/config/custom_components/hello_world.py b/config/custom_components/hello_world.py index 96d9a788b6b..a3d4ce762bb 100644 --- a/config/custom_components/hello_world.py +++ b/config/custom_components/hello_world.py @@ -1,16 +1,14 @@ """ custom_components.hello_world ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Implements the bare minimum that a component should implement. Configuration: To use the hello_word component you will need to add the following to your -config/configuration.yaml +configuration.yaml file. hello_world: - """ # The domain of your component. Should be equal to the name of your component diff --git a/config/custom_components/mqtt_example.py b/config/custom_components/mqtt_example.py index 5b54226cb7c..98e16b6bfa9 100644 --- a/config/custom_components/mqtt_example.py +++ b/config/custom_components/mqtt_example.py @@ -1,7 +1,6 @@ """ custom_components.mqtt_example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Shows how to communicate with MQTT. Follows a topic on MQTT and updates the state of an entity to the last message received on that topic. @@ -12,7 +11,7 @@ example payload {"new_state": "some new state"}. Configuration: To use the mqtt_example component you will need to add the following to your -config/configuration.yaml +configuration.yaml file. mqtt_example: topic: home-assistant/mqtt_example diff --git a/docs/architecture-remote.png b/docs/architecture-remote.png deleted file mode 100644 index 3109c921846..00000000000 Binary files a/docs/architecture-remote.png and /dev/null differ diff --git a/docs/architecture.png b/docs/architecture.png deleted file mode 100644 index 7fe62cf3144..00000000000 Binary files a/docs/architecture.png and /dev/null differ diff --git a/docs/screenshots.png b/docs/screenshots.png index 09dff77c894..a5e278b0394 100644 Binary files a/docs/screenshots.png and b/docs/screenshots.png differ diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 2514b35587f..2641961f5c3 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -4,12 +4,10 @@ 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')) +from homeassistant import bootstrap +import homeassistant.config as config_util +from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START def validate_python(): @@ -18,106 +16,58 @@ def validate_python(): 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() + sys.exit(1) 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('--version', action='version', version=__version__) 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', @@ -127,40 +77,115 @@ def get_arguments(): '--open-ui', action='store_true', help='Open the webinterface in a browser') + parser.add_argument( + '--skip-pip', + action='store_true', + help='Skips pip install of required packages on startup') + parser.add_argument( + '-v', '--verbose', + action='store_true', + help="Enable verbose logging to file.") + parser.add_argument( + '--pid-file', + metavar='path_to_pid_file', + default=None, + help='Path to PID file useful for running as daemon') + parser.add_argument( + '--log-rotate-days', + type=int, + default=None, + help='Enables daily log rotation and keeps up to the specified days') + if os.name != "nt": + parser.add_argument( + '--daemon', + action='store_true', + help='Run Home Assistant as daemon') - return parser.parse_args() + arguments = parser.parse_args() + if os.name == "nt": + arguments.daemon = False + return arguments + + +def daemonize(): + """ Move current process to daemon process """ + # create first fork + pid = os.fork() + if pid > 0: + sys.exit(0) + + # decouple fork + os.setsid() + os.umask(0) + + # create second fork + pid = os.fork() + if pid > 0: + sys.exit(0) + + +def check_pid(pid_file): + """ Check that HA is not already running """ + # check pid file + try: + pid = int(open(pid_file, 'r').readline()) + except IOError: + # PID File does not exist + return + + try: + os.kill(pid, 0) + except OSError: + # PID does not exist + return + print('Fatal Error: HomeAssistant is already running.') + sys.exit(1) + + +def write_pid(pid_file): + """ Create PID File """ + pid = os.getpid() + try: + open(pid_file, 'w').write(str(pid)) + except IOError: + print('Fatal Error: Unable to write pid file {}'.format(pid_file)) + sys.exit(1) 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) + + # daemon functions + if args.pid_file: + check_pid(args.pid_file) + if args.daemon: + daemonize() + if args.pid_file: + write_pid(args.pid_file) if args.demo_mode: - from homeassistant.components import frontend, demo - - hass = bootstrap.from_config_dict({ - frontend.DOMAIN: {}, - demo.DOMAIN: {} - }) + config = { + 'frontend': {}, + 'demo': {} + } + hass = bootstrap.from_config_dict( + config, config_dir=config_dir, daemon=args.daemon, + verbose=args.verbose, skip_pip=args.skip_pip, + log_rotate_days=args.log_rotate_days) else: - hass = bootstrap.from_config_file(config_path) + config_file = ensure_config_file(config_dir) + print('Config directory:', config_dir) + hass = bootstrap.from_config_file( + config_file, daemon=args.daemon, verbose=args.verbose, + skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) 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..a7e4dbfdc14 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -10,7 +10,9 @@ start by calling homeassistant.start_home_assistant(bus) """ import os +import sys import logging +import logging.handlers from collections import defaultdict import homeassistant.core as core @@ -52,22 +54,19 @@ def setup_component(hass, domain, config=None): return False for component in components: - if component in hass.config.components: - continue - if not _setup_component(hass, component, config): return False return True -def _handle_requirements(component, name): +def _handle_requirements(hass, component, name): """ Installs requirements for component. """ - if not hasattr(component, 'REQUIREMENTS'): + if hass.config.skip_pip or 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 @@ -77,6 +76,8 @@ def _handle_requirements(component, name): def _setup_component(hass, domain, config): """ Setup a component for Home Assistant. """ + if domain in hass.config.components: + return True component = loader.get_component(domain) missing_deps = [dep for dep in component.DEPENDENCIES @@ -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: @@ -122,6 +123,7 @@ def prepare_setup_platform(hass, config, domain, platform_name): # Not found if platform is None: + _LOGGER.error('Unable to find platform %s', platform_path) return None # Already loaded @@ -138,14 +140,21 @@ 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 -# pylint: disable=too-many-branches, too-many-statements -def from_config_dict(config, hass=None): +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, too-many-arguments +def from_config_dict(config, hass=None, config_dir=None, enable_log=True, + verbose=False, daemon=False, skip_pip=False, + log_rotate_days=None): """ Tries to configure Home Assistant from a config dict. @@ -153,10 +162,20 @@ 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, verbose, daemon, log_rotate_days) + + hass.config.skip_pip = skip_pip + if skip_pip: + _LOGGER.warning('Skipping pip installation of required modules. ' + 'This may cause issues.') _ensure_loader_prepared(hass) @@ -185,7 +204,8 @@ def from_config_dict(config, hass=None): return hass -def from_config_file(config_path, hass=None): +def from_config_file(config_path, hass=None, verbose=False, daemon=False, + skip_pip=True, log_rotate_days=None): """ Reads the configuration file and tries to start all the required functionality. Will add functionality to 'hass' parameter if given, @@ -195,35 +215,41 @@ 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, verbose, daemon, log_rotate_days) 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, + skip_pip=skip_pip) -def enable_logging(hass): +def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None): """ Setup the logging for home assistant. """ - logging.basicConfig(level=logging.INFO) - fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " - "[%(name)s] %(message)s%(reset)s") - try: - from colorlog import ColoredFormatter - logging.getLogger().handlers[0].setFormatter(ColoredFormatter( - fmt, - datefmt='%y-%m-%d %H:%M:%S', - reset=True, - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', - } - )) - except ImportError: - _LOGGER.warning( - "Colorlog package not found, console coloring disabled") + if not daemon: + logging.basicConfig(level=logging.INFO) + fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " + "[%(name)s] %(message)s%(reset)s") + try: + from colorlog import ColoredFormatter + logging.getLogger().handlers[0].setFormatter(ColoredFormatter( + fmt, + datefmt='%y-%m-%d %H:%M:%S', + reset=True, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red', + } + )) + except ImportError: + _LOGGER.warning( + "Colorlog package not found, console coloring disabled") # Log errors to a file if we have write access to file or config dir err_log_path = hass.config.path('home-assistant.log') @@ -234,14 +260,20 @@ def enable_logging(hass): if (err_path_exists and os.access(err_log_path, os.W_OK)) or \ (not err_path_exists and os.access(hass.config.config_dir, os.W_OK)): - err_handler = logging.FileHandler( - err_log_path, mode='w', delay=True) + if log_rotate_days: + err_handler = logging.handlers.TimedRotatingFileHandler( + err_log_path, when='midnight', backupCount=log_rotate_days) + else: + err_handler = logging.FileHandler( + err_log_path, mode='w', delay=True) - err_handler.setLevel(logging.WARNING) + err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter( logging.Formatter('%(asctime)s %(name)s: %(message)s', datefmt='%y-%m-%d %H:%M:%S')) - logging.getLogger('').addHandler(err_handler) + logger = logging.getLogger('') + logger.addHandler(err_handler) + logger.setLevel(logging.INFO) # this sets the minimum log level else: _LOGGER.error( diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index db91c5e0d9c..cbb319e2541 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -7,7 +7,7 @@ runs with the Firmata firmware. Configuration: To use the Arduino board you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. arduino: port: /dev/ttyACM0 diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index f7cf5654f46..7e4a24ffdfe 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -1,4 +1,6 @@ """ +homeassistant.components.camera.generic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for IP Cameras. This component provides basic support for IP cameras. For the basic support to @@ -7,11 +9,11 @@ need to specify the "still_image_url" parameter which should be the location of the JPEG image. As part of the basic support the following features will be provided: --MJPEG video streaming --Saving a snapshot --Recording(JPEG frame capture) +- MJPEG video streaming +- Saving a snapshot +- Recording(JPEG frame capture) -To use this component, add the following to your config/configuration.yaml: +To use this component, add the following to your configuration.yaml file. camera: platform: generic @@ -20,29 +22,24 @@ camera: password: YOUR_PASSWORD still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg - -VARIABLES: - -These are the variables for the device_data array: +Variables: still_image_url *Required -The URL your camera serves the image on. -Example: http://192.168.1.21:2112/ +The URL your camera serves the image on, eg. http://192.168.1.21:2112/ name *Optional -This parameter allows you to override the name of your camera in homeassistant +This parameter allows you to override the name of your camera in Home +Assistant. username *Optional -THe username for acessing your camera +The username for accessing your camera. password *Optional -the password for accessing your camera - - +The password for accessing your camera. """ import logging from requests.auth import HTTPBasicAuth @@ -78,7 +75,7 @@ class GenericCamera(Camera): self._still_image_url = device_info['still_image_url'] def camera_image(self): - """ Return a still image reponse from the camera """ + """ Return a still image reponse from the camera. """ if self._username and self._password: response = requests.get( self._still_image_url, @@ -90,5 +87,5 @@ class GenericCamera(Camera): @property def name(self): - """ Return the name of this device """ + """ Return the name of this device. """ return self._name diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 2d439a7ac4a..fd2ad60d211 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -10,7 +10,7 @@ import re from homeassistant import core from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) DOMAIN = "conversation" DEPENDENCIES = [] @@ -44,7 +44,7 @@ def setup(hass, config): entity_ids = [ state.entity_id for state in hass.states.all() - if state.attributes.get(ATTR_FRIENDLY_NAME, "").lower() == name] + if state.name.lower() == name] if not entity_ids: logger.error( diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 2dab6a12659..beb7a63b47c 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -14,10 +14,10 @@ from homeassistant.const import ( DOMAIN = "demo" -DEPENDENCIES = [] +DEPENDENCIES = ['introduction', 'conversation'] COMPONENTS_WITH_DEMO_PLATFORM = [ - 'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify'] + 'switch', 'light', 'sensor', 'thermostat', 'media_player', 'notify'] def setup(hass, config): @@ -46,12 +46,12 @@ def setup(hass, config): hass, component, {component: {CONF_PLATFORM: 'demo'}}) # Setup room groups - lights = hass.states.entity_ids('light') - switches = hass.states.entity_ids('switch') + lights = sorted(hass.states.entity_ids('light')) + switches = sorted(hass.states.entity_ids('switch')) media_players = sorted(hass.states.entity_ids('media_player')) - group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0], + group.setup_group(hass, 'living room', [lights[2], lights[1], switches[0], media_players[1]]) - group.setup_group(hass, 'bedroom', [lights[2], switches[1], + group.setup_group(hass, 'bedroom', [lights[0], switches[1], media_players[0]]) # Setup IP Camera @@ -68,7 +68,7 @@ def setup(hass, config): hass, 'script', {'script': { 'demo': { - 'alias': 'Demo {}'.format(lights[0]), + 'alias': 'Toggle {}'.format(lights[0].split('.')[1]), 'sequence': [{ 'execute_service': 'light.turn_off', 'service_data': {ATTR_ENTITY_ID: lights[0]} diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index e529327f505..c7dc2593ddb 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,51 +1,82 @@ """ -homeassistant.components.tracker -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +homeassistant.components.device_tracker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to keep track of devices. + +device_tracker: + platform: netgear + + # Optional + + # How many seconds to wait after not seeing device to consider it not home + consider_home: 180 + + # Seconds between each scan + interval_seconds: 12 + + # New found devices auto found + track_new_devices: yes """ -import logging -import threading -import os import csv from datetime import timedelta +import logging +import os +import threading -from homeassistant.loader import get_component -from homeassistant.helpers import validate_config +from homeassistant.bootstrap import prepare_setup_platform +from homeassistant.components import discovery, group +from homeassistant.config import load_yaml_config_file +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.entity import Entity import homeassistant.util as util import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( - STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, - CONF_PLATFORM, DEVICE_DEFAULT_NAME) -from homeassistant.components import group + ATTR_ENTITY_PICTURE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) DOMAIN = "device_tracker" DEPENDENCIES = [] -SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" - GROUP_NAME_ALL_DEVICES = 'all devices' ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') ENTITY_ID_FORMAT = DOMAIN + '.{}' -# After how much time do we consider a device not home if -# it does not show up on scans -TIME_DEVICE_NOT_FOUND = timedelta(minutes=3) +CSV_DEVICES = "known_devices.csv" +YAML_DEVICES = 'known_devices.yaml' -# Filename to save known devices to -KNOWN_DEVICES_FILE = "known_devices.csv" +CONF_TRACK_NEW = "track_new_devices" +DEFAULT_CONF_TRACK_NEW = True -CONF_SECONDS = "interval_seconds" +CONF_CONSIDER_HOME = 'consider_home' +DEFAULT_CONF_CONSIDER_HOME = 180 # seconds -DEFAULT_CONF_SECONDS = 12 +CONF_SCAN_INTERVAL = "interval_seconds" +DEFAULT_SCAN_INTERVAL = 12 -TRACK_NEW_DEVICES = "track_new_devices" +CONF_AWAY_HIDE = 'hide_if_away' +DEFAULT_AWAY_HIDE = False +SERVICE_SEE = 'see' + +ATTR_LATITUDE = 'latitude' +ATTR_LONGITUDE = 'longitude' +ATTR_MAC = 'mac' +ATTR_DEV_ID = 'dev_id' +ATTR_HOST_NAME = 'host_name' +ATTR_LOCATION_NAME = 'location_name' +ATTR_GPS = 'gps' + +DISCOVERY_PLATFORMS = { + discovery.SERVICE_NETGEAR: 'netgear', +} _LOGGER = logging.getLogger(__name__) +# pylint: disable=too-many-arguments + def is_on(hass, entity_id=None): """ Returns if any or specified device is home. """ @@ -54,290 +85,309 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity, STATE_HOME) +def see(hass, mac=None, dev_id=None, host_name=None, location_name=None, + gps=None): + """ Call service to notify you see device. """ + data = {key: value for key, value in + ((ATTR_MAC, mac), + (ATTR_DEV_ID, dev_id), + (ATTR_HOST_NAME, host_name), + (ATTR_LOCATION_NAME, location_name), + (ATTR_GPS, gps)) if value is not None} + hass.services.call(DOMAIN, SERVICE_SEE, data) + + def setup(hass, config): - """ Sets up the device tracker. """ + """ Setup device tracker """ + yaml_path = hass.config.path(YAML_DEVICES) + csv_path = hass.config.path(CSV_DEVICES) + if os.path.isfile(csv_path) and not os.path.isfile(yaml_path) and \ + convert_csv_config(csv_path, yaml_path): + os.remove(csv_path) - if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER): - return False + conf = config.get(DOMAIN, {}) + consider_home = util.convert(conf.get(CONF_CONSIDER_HOME), int, + DEFAULT_CONF_CONSIDER_HOME) + track_new = util.convert(conf.get(CONF_TRACK_NEW), bool, + DEFAULT_CONF_TRACK_NEW) - tracker_type = config[DOMAIN].get(CONF_PLATFORM) + devices = load_config(yaml_path, hass, timedelta(seconds=consider_home)) + tracker = DeviceTracker(hass, consider_home, track_new, devices) - tracker_implementation = get_component( - 'device_tracker.{}'.format(tracker_type)) - - if tracker_implementation is None: - _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: %s", - tracker_type) - - return False - - seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int, - DEFAULT_CONF_SECONDS) - - track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False - _LOGGER.info("Tracking new devices: %s", track_new_devices) - - tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices) - - # We only succeeded if we got to parse the known devices file - return not tracker.invalid_known_devices_file - - -class DeviceTracker(object): - """ Class that tracks which devices are home and which are not. """ - - def __init__(self, hass, device_scanner, seconds, track_new_devices): - self.hass = hass - - self.device_scanner = device_scanner - - self.lock = threading.Lock() - - # Do we track new devices by default? - self.track_new_devices = track_new_devices - - # Dictionary to keep track of known devices and devices we track - self.tracked = {} - self.untracked_devices = set() - - # Did we encounter an invalid known devices file - self.invalid_known_devices_file = False - - # Wrap it in a func instead of lambda so it can be identified in - # the bus by its __name__ attribute. - def update_device_state(now): - """ Triggers update of the device states. """ - self.update_devices(now) - - dev_group = group.Group( - hass, GROUP_NAME_ALL_DEVICES, user_defined=False) - - def reload_known_devices_service(service): - """ Reload known devices file. """ - self._read_known_devices_file() - - self.update_devices(dt_util.utcnow()) - - dev_group.update_tracked_entity_ids(self.device_entity_ids) - - reload_known_devices_service(None) - - if self.invalid_known_devices_file: - return - - seconds = range(0, 60, seconds) - - _LOGGER.info("Device tracker interval second=%s", seconds) - track_utc_time_change(hass, update_device_state, second=seconds) - - hass.services.register(DOMAIN, - SERVICE_DEVICE_TRACKER_RELOAD, - reload_known_devices_service) - - @property - def device_entity_ids(self): - """ Returns a set containing all device entity ids - that are being tracked. """ - return set(device['entity_id'] for device in self.tracked.values()) - - def _update_state(self, now, device, is_home): - """ Update the state of a device. """ - dev_info = self.tracked[device] - - if is_home: - # Update last seen if at home - dev_info['last_seen'] = now - else: - # State remains at home if it has been seen in the last - # TIME_DEVICE_NOT_FOUND - is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND - - state = STATE_HOME if is_home else STATE_NOT_HOME - - self.hass.states.set( - dev_info['entity_id'], state, - dev_info['state_attr']) - - def update_devices(self, now): - """ Update device states based on the found devices. """ - if not self.lock.acquire(False): + def setup_platform(p_type, p_config, disc_info=None): + """ Setup a device tracker platform. """ + platform = prepare_setup_platform(hass, config, DOMAIN, p_type) + if platform is None: return try: - found_devices = set(dev.upper() for dev in - self.device_scanner.scan_devices()) + if hasattr(platform, 'get_scanner'): + scanner = platform.get_scanner(hass, {DOMAIN: p_config}) - for device in self.tracked: - is_home = device in found_devices + if scanner is None: + _LOGGER.error('Error setting up platform %s', p_type) + return - self._update_state(now, device, is_home) + setup_scanner_platform(hass, p_config, scanner, tracker.see) + return - if is_home: - found_devices.remove(device) + if not platform.setup_scanner(hass, p_config, tracker.see): + _LOGGER.error('Error setting up platform %s', p_type) + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error setting up platform %s', p_type) - # Did we find any devices that we didn't know about yet? - new_devices = found_devices - self.untracked_devices + for p_type, p_config in \ + config_per_platform(config, DOMAIN, _LOGGER): + setup_platform(p_type, p_config) - if new_devices: - if not self.track_new_devices: - self.untracked_devices.update(new_devices) + def device_tracker_discovered(service, info): + """ Called when a device tracker platform is discovered. """ + setup_platform(DISCOVERY_PLATFORMS[service], {}, info) - self._update_known_devices_file(new_devices) - finally: - self.lock.release() + discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), + device_tracker_discovered) - # pylint: disable=too-many-branches - def _read_known_devices_file(self): - """ Parse and process the known devices file. """ - known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) + def update_stale(now): + """ Clean up stale devices. """ + tracker.update_stale(now) + track_utc_time_change(hass, update_stale, second=range(0, 60, 5)) - # Return if no known devices file exists - if not os.path.isfile(known_dev_path): + tracker.setup_group() + + def see_service(call): + """ Service to see a device. """ + args = {key: value for key, value in call.data.items() if key in + (ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME, + ATTR_GPS)} + tracker.see(**args) + + hass.services.register(DOMAIN, SERVICE_SEE, see_service) + + return True + + +class DeviceTracker(object): + """ Track devices """ + def __init__(self, hass, consider_home, track_new, devices): + self.hass = hass + self.devices = {dev.dev_id: dev for dev in devices} + self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} + self.consider_home = timedelta(seconds=consider_home) + self.track_new = track_new + self.lock = threading.Lock() + + entity_ids = [] + for device in devices: + if device.track: + entity_ids.append(device.entity_id) + device.update_ha_state() + + self.group = None + + def see(self, mac=None, dev_id=None, host_name=None, location_name=None, + gps=None): + """ Notify device tracker that you see a device. """ + with self.lock: + if mac is None and dev_id is None: + raise HomeAssistantError('Neither mac or device id passed in') + elif mac is not None: + mac = mac.upper() + device = self.mac_to_dev.get(mac) + if not device: + dev_id = util.slugify(host_name or mac) + else: + dev_id = str(dev_id) + device = self.devices.get(dev_id) + + if device: + device.seen(host_name, location_name, gps) + if device.track: + device.update_ha_state() + return + + # If no device can be found, create it + device = Device( + self.hass, self.consider_home, self.track_new, dev_id, mac, + (host_name or dev_id).replace('_', ' ')) + self.devices[dev_id] = device + if mac is not None: + self.mac_to_dev[mac] = device + + device.seen(host_name, location_name, gps) + if device.track: + device.update_ha_state() + + # During init, we ignore the group + if self.group is not None: + self.group.update_tracked_entity_ids( + list(self.group.tracking) + [device.entity_id]) + update_config(self.hass.config.path(YAML_DEVICES), dev_id, device) + + def setup_group(self): + """ Initializes group for all tracked devices. """ + entity_ids = (dev.entity_id for dev in self.devices.values() + if dev.track) + self.group = group.setup_group( + self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) + + def update_stale(self, now): + """ Update stale devices. """ + with self.lock: + for device in self.devices.values(): + if device.last_update_home and device.stale(now): + device.update_ha_state(True) + + +class Device(Entity): + """ Tracked device. """ + # pylint: disable=too-many-instance-attributes, too-many-arguments + + host_name = None + location_name = None + gps = None + last_seen = None + + # Track if the last update of this device was HOME + last_update_home = False + _state = STATE_NOT_HOME + + def __init__(self, hass, consider_home, track, dev_id, mac, name=None, + picture=None, away_hide=False): + self.hass = hass + self.entity_id = ENTITY_ID_FORMAT.format(dev_id) + + # Timedelta object how long we consider a device home if it is not + # detected anymore. + self.consider_home = consider_home + + # Device ID + self.dev_id = dev_id + self.mac = mac + + # If we should track this device + self.track = track + + # Configured name + self.config_name = name + + # Configured picture + self.config_picture = picture + self.away_hide = away_hide + + @property + def name(self): + """ Returns the name of the entity. """ + return self.config_name or self.host_name or DEVICE_DEFAULT_NAME + + @property + def state(self): + """ State of the device. """ + return self._state + + @property + def state_attributes(self): + """ Device state attributes. """ + attr = {} + + if self.config_picture: + attr[ATTR_ENTITY_PICTURE] = self.config_picture + + if self.gps: + attr[ATTR_LATITUDE] = self.gps[0], + attr[ATTR_LONGITUDE] = self.gps[1], + + return attr + + @property + def hidden(self): + """ If device should be hidden. """ + return self.away_hide and self.state != STATE_HOME + + def seen(self, host_name=None, location_name=None, gps=None): + """ Mark the device as seen. """ + self.last_seen = dt_util.utcnow() + self.host_name = host_name + self.location_name = location_name + self.gps = gps + self.update() + + def stale(self, now=None): + """ Return if device state is stale. """ + return self.last_seen and \ + (now or dt_util.utcnow()) - self.last_seen > self.consider_home + + def update(self): + """ Update state of entity. """ + if not self.last_seen: return + elif self.location_name: + self._state = self.location_name + elif self.stale(): + self._state = STATE_NOT_HOME + self.last_update_home = False + else: + self._state = STATE_HOME + self.last_update_home = True - self.lock.acquire() - self.untracked_devices.clear() +def convert_csv_config(csv_path, yaml_path): + """ Convert CSV config file format to YAML. """ + used_ids = set() + with open(csv_path) as inp: + for row in csv.DictReader(inp): + dev_id = util.ensure_unique_string( + util.slugify(row['name']) or DEVICE_DEFAULT_NAME, used_ids) + used_ids.add(dev_id) + device = Device(None, None, row['track'] == '1', dev_id, + row['device'], row['name'], row['picture']) + update_config(yaml_path, dev_id, device) + return True - with open(known_dev_path) as inp: - # To track which devices need an entity_id assigned - need_entity_id = [] +def load_config(path, hass, consider_home): + """ Load devices from YAML config file. """ + if not os.path.isfile(path): + return [] + return [ + Device(hass, consider_home, device.get('track', False), + str(dev_id), device.get('mac'), device.get('name'), + device.get('picture'), device.get(CONF_AWAY_HIDE, False)) + for dev_id, device in load_yaml_config_file(path).items()] - # All devices that are still in this set after we read the CSV file - # have been removed from the file and thus need to be cleaned up. - removed_devices = set(self.tracked.keys()) - try: - for row in csv.DictReader(inp): - device = row['device'].upper() +def setup_scanner_platform(hass, config, scanner, see_device): + """ Helper method to connect scanner-based platform to device tracker. """ + interval = util.convert(config.get(CONF_SCAN_INTERVAL), int, + DEFAULT_SCAN_INTERVAL) - if row['track'] == '1': - if device in self.tracked: - # Device exists - removed_devices.remove(device) - else: - # We found a new device - need_entity_id.append(device) + # Initial scan of each mac we also tell about host name for config + seen = set() - self._track_device(device, row['name']) + def device_tracker_scan(now): + """ Called when interval matches. """ + for mac in scanner.scan_devices(): + if mac in seen: + host_name = None + else: + host_name = scanner.get_device_name(mac) + seen.add(mac) + see_device(mac=mac, host_name=host_name) - # Update state_attr with latest from file - state_attr = { - ATTR_FRIENDLY_NAME: row['name'] - } + track_utc_time_change(hass, device_tracker_scan, second=range(0, 60, + interval)) - if row['picture']: - state_attr[ATTR_ENTITY_PICTURE] = row['picture'] + device_tracker_scan(None) - self.tracked[device]['state_attr'] = state_attr - else: - self.untracked_devices.add(device) +def update_config(path, dev_id, device): + """ Add device to YAML config file. """ + with open(path, 'a') as out: + out.write('\n') + out.write('{}:\n'.format(device.dev_id)) - # Remove existing devices that we no longer track - for device in removed_devices: - entity_id = self.tracked[device]['entity_id'] - - _LOGGER.info("Removing entity %s", entity_id) - - self.hass.states.remove(entity_id) - - self.tracked.pop(device) - - self._generate_entity_ids(need_entity_id) - - if not self.tracked: - _LOGGER.warning( - "No devices to track. Please update %s.", - known_dev_path) - - _LOGGER.info("Loaded devices from %s", known_dev_path) - - except KeyError: - self.invalid_known_devices_file = True - - _LOGGER.warning( - ("Invalid known devices file: %s. " - "We won't update it with new found devices."), - known_dev_path) - - finally: - self.lock.release() - - def _update_known_devices_file(self, new_devices): - """ Add new devices to known devices file. """ - if not self.invalid_known_devices_file: - known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) - - try: - # If file does not exist we will write the header too - is_new_file = not os.path.isfile(known_dev_path) - - with open(known_dev_path, 'a') as outp: - _LOGGER.info("Found %d new devices, updating %s", - len(new_devices), known_dev_path) - - writer = csv.writer(outp) - - if is_new_file: - writer.writerow(("device", "name", "track", "picture")) - - for device in new_devices: - # See if the device scanner knows the name - # else defaults to unknown device - name = self.device_scanner.get_device_name(device) or \ - DEVICE_DEFAULT_NAME - - track = 0 - if self.track_new_devices: - self._track_device(device, name) - track = 1 - - writer.writerow((device, name, track, "")) - - if self.track_new_devices: - self._generate_entity_ids(new_devices) - - except IOError: - _LOGGER.exception("Error updating %s with %d new devices", - known_dev_path, len(new_devices)) - - def _track_device(self, device, name): - """ - Add a device to the list of tracked devices. - Does not generate the entity id yet. - """ - default_last_seen = dt_util.utcnow().replace(year=1990) - - self.tracked[device] = { - 'name': name, - 'last_seen': default_last_seen, - 'state_attr': {ATTR_FRIENDLY_NAME: name} - } - - def _generate_entity_ids(self, need_entity_id): - """ Generate entity ids for a list of devices. """ - # Setup entity_ids for the new devices - used_entity_ids = [info['entity_id'] for device, info - in self.tracked.items() - if device not in need_entity_id] - - for device in need_entity_id: - name = self.tracked[device]['name'] - - entity_id = util.ensure_unique_string( - ENTITY_ID_FORMAT.format(util.slugify(name)), - used_entity_ids) - - used_entity_ids.append(entity_id) - - self.tracked[device]['entity_id'] = entity_id + for key, value in (('name', device.name), ('mac', device.mac), + ('picture', device.config_picture), + ('track', 'yes' if device.track else 'no'), + (CONF_AWAY_HIDE, + 'yes' if device.away_hide else 'no')): + out.write(' {}: {}\n'.format(key, '' if value is None else value)) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 06956475ba0..f926b182983 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.actiontec -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Device tracker platform that supports scanning an Actiontec MI424WR (Verizon FIOS) router for device presence. @@ -9,13 +9,16 @@ 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 +following to your configuration.yaml file. If you experience disconnects +you can modify the home_interval variable. device_tracker: platform: actiontec host: YOUR_ROUTER_IP username: YOUR_ADMIN_USERNAME password: YOUR_ADMIN_PASSWORD + # optional: + home_interval: 10 Variables: @@ -30,21 +33,32 @@ The username of an user with administrative privileges, usually 'admin'. password *Required The password for your given admin account. + +home_interval +*Optional +If the home_interval is set then the component will not let a device +be AWAY if it has been HOME in the last home_interval minutes. This is +in addition to the 3 minute wait built into the device_tracker component. """ import logging from datetime import timedelta +from collections import namedtuple import re import threading import telnetlib +import homeassistant.util.dt as dt_util from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import validate_config -from homeassistant.util import Throttle +from homeassistant.util import Throttle, convert 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) +# interval in minutes to exclude devices from a scan while they are home +CONF_HOME_INTERVAL = "home_interval" + _LOGGER = logging.getLogger(__name__) _LEASES_REGEX = re.compile( @@ -54,7 +68,7 @@ _LEASES_REGEX = re.compile( # pylint: disable=unused-argument def get_scanner(hass, config): - """ Validates config and returns a DD-WRT scanner. """ + """ Validates config and returns an Actiontec scanner. """ if not validate_config(config, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, _LOGGER): @@ -64,59 +78,87 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None +Device = namedtuple("Device", ["mac", "ip", "last_update"]) + class ActiontecDeviceScanner(object): - """ This class queries a an actiontec router - for connected devices. Adapted from DD-WRT scanner. + """ + 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] + minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) + self.home_interval = timedelta(minutes=minutes) self.lock = threading.Lock() - self.last_results = {} + self.last_results = [] # Test the router is accessible data = self.get_actiontec_data() self.success_init = data is not None + _LOGGER.info("actiontec scanner initialized") + if self.home_interval: + _LOGGER.info("home_interval set to: %s", self.home_interval) def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() - return [client['mac'] for client in self.last_results] + 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'] + 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. """ + """ + Ensures the information from the Actiontec MI424WR router is up + to date. Returns boolean if scanning successful. + """ + _LOGGER.info("Scanning") if not self.success_init: return False with self.lock: - # _LOGGER.info("Checking ARP") - data = self.get_actiontec_data() - if not data: + exclude_targets = set() + exclude_target_list = [] + now = dt_util.now() + if self.home_interval: + for host in self.last_results: + if host.last_update + self.home_interval > now: + exclude_targets.add(host) + if len(exclude_targets) > 0: + exclude_target_list = [t.ip for t in exclude_targets] + + actiontec_data = self.get_actiontec_data() + if not actiontec_data: return False - active_clients = [client for client in data.values()] - self.last_results = active_clients + self.last_results = [] + for client in exclude_target_list: + if client in actiontec_data: + actiontec_data.pop(client) + for name, data in actiontec_data.items(): + device = Device(data['mac'], name, now) + self.last_results.append(device) + self.last_results.extend(exclude_targets) + _LOGGER.info("actiontec scan successful") return True def get_actiontec_data(self): - """ Retrieve data from Actiontec MI424WR and return parsed result. """ + """ Retrieve data from Actiontec MI424WR and return parsed result. """ try: telnet = telnetlib.Telnet(self.host) telnet.read_until(b'Username: ') diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py new file mode 100644 index 00000000000..68ff8390216 --- /dev/null +++ b/homeassistant/components/device_tracker/aruba.py @@ -0,0 +1,148 @@ +""" +homeassistant.components.device_tracker.aruba +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a Aruba Access Point for device +presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the Aruba tracker you will need to add something like the following +to your configuration.yaml file. You also need to enable Telnet in the +configuration page of your router. + +device_tracker: + platform: aruba + host: YOUR_ACCESS_POINT_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([^\s]+))\s+' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s+') + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a Aruba scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ArubaDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ArubaDeviceScanner(object): + """ This class queries a Aruba Acces Point for connected devices. """ + 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_aruba_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['name'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensures the information from the Aruba Access Point is up to date. + Returns boolean if scanning successful. + """ + if not self.success_init: + return False + + with self.lock: + data = self.get_aruba_data() + if not data: + return False + + self.last_results = data.values() + return True + + def get_aruba_data(self): + """ Retrieve data from Aruba Access Point and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'User: ') + 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(('show clients\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(), + 'name': match.group('name') + } + return devices diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index fdf2ca70eaa..c0b29ab420f 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -9,7 +9,7 @@ This device tracker needs telnet to be enabled on the router. Configuration: To use the ASUSWRT tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: asuswrt @@ -63,7 +63,7 @@ _IP_NEIGH_REGEX = re.compile( # pylint: disable=unused-argument def get_scanner(hass, config): - """ Validates config and returns a DD-WRT scanner. """ + """ Validates config and returns an ASUS-WRT scanner. """ if not validate_config(config, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, _LOGGER): @@ -75,7 +75,8 @@ def get_scanner(hass, config): class AsusWrtDeviceScanner(object): - """ This class queries a router running ASUSWRT firmware + """ + This class queries a router running ASUSWRT firmware for connected devices. Adapted from DD-WRT scanner. """ @@ -93,8 +94,9 @@ class AsusWrtDeviceScanner(object): self.success_init = data is not None def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device IDs. + """ self._update_info() return [client['mac'] for client in self.last_results] @@ -110,8 +112,10 @@ class AsusWrtDeviceScanner(object): @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Ensures the information from the ASUSWRT router is up to date. - Returns boolean if scanning successful. """ + """ + Ensures the information from the ASUSWRT router is up to date. + Returns boolean if scanning successful. + """ if not self.success_init: return False @@ -129,7 +133,7 @@ class AsusWrtDeviceScanner(object): return True def get_asuswrt_data(self): - """ Retrieve data from ASUSWRT and return parsed result. """ + """ Retrieve data from ASUSWRT and return parsed result. """ try: telnet = telnetlib.Telnet(self.host) telnet.read_until(b'login: ') diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 2c69746fab0..a9a4ac8e3f5 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -1,14 +1,13 @@ """ homeassistant.components.device_tracker.ddwrt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Device tracker platform that supports scanning a DD-WRT router for device presence. Configuration: To use the DD-WRT tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: ddwrt @@ -64,7 +63,8 @@ def get_scanner(hass, config): # pylint: disable=too-many-instance-attributes class DdWrtDeviceScanner(object): - """ This class queries a wireless router running DD-WRT firmware + """ + This class queries a wireless router running DD-WRT firmware for connected devices. Adapted from Tomato scanner. """ @@ -85,8 +85,9 @@ class DdWrtDeviceScanner(object): self.success_init = data is not None def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() @@ -124,8 +125,10 @@ class DdWrtDeviceScanner(object): @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Ensures the information from the DD-WRT router is up to date. - Returns boolean if scanning successful. """ + """ + Ensures the information from the DD-WRT router is up to date. + Returns boolean if scanning successful. + """ if not self.success_init: return False @@ -163,7 +166,7 @@ class DdWrtDeviceScanner(object): return False def get_ddwrt_data(self, url): - """ Retrieve data from DD-WRT and return parsed result. """ + """ Retrieve data from DD-WRT and return parsed result. """ try: response = requests.get( url, diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 893c9070526..4cbc6a2d492 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -1,18 +1,16 @@ """ homeassistant.components.device_tracker.luci ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Device tracker platform that supports scanning a OpenWRT router for device presence. - It's required that the luci RPC package is installed on the OpenWRT router: # opkg install luci-mod-rpc Configuration: To use the Luci tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: luci @@ -66,7 +64,8 @@ def get_scanner(hass, config): # pylint: disable=too-many-instance-attributes class LuciDeviceScanner(object): - """ This class queries a wireless router running OpenWrt firmware + """ + This class queries a wireless router running OpenWrt firmware for connected devices. Adapted from Tomato scanner. # opkg install luci-mod-rpc @@ -95,8 +94,9 @@ class LuciDeviceScanner(object): self.success_init = self.token is not None def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() @@ -124,8 +124,10 @@ class LuciDeviceScanner(object): @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Ensures the information from the Luci router is up to date. - Returns boolean if scanning successful. """ + """ + Ensures the information from the Luci router is up to date. + Returns boolean if scanning successful. + """ if not self.success_init: return False @@ -179,6 +181,6 @@ def _req_json_rpc(url, method, *args, **kwargs): def _get_token(host, username, password): - """ Get authentication token for the given host+username+password """ + """ Get authentication token for the given host+username+password. """ url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host) return _req_json_rpc(url, 'login', username, password) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py new file mode 100644 index 00000000000..34cee8f6733 --- /dev/null +++ b/homeassistant/components/device_tracker/mqtt.py @@ -0,0 +1,48 @@ +""" +homeassistant.components.device_tracker.mqtt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +MQTT platform for the device tracker. + +device_tracker: + platform: mqtt + qos: 1 + devices: + paulus_oneplus: /location/paulus + annetherese_n4: /location/annetherese +""" +import logging +from homeassistant import util +import homeassistant.components.mqtt as mqtt + +DEPENDENCIES = ['mqtt'] + +CONF_QOS = 'qos' +CONF_DEVICES = 'devices' + +DEFAULT_QOS = 0 + +_LOGGER = logging.getLogger(__name__) + + +def setup_scanner(hass, config, see): + """ Set up a MQTT tracker. """ + devices = config.get(CONF_DEVICES) + qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS) + + if not isinstance(devices, dict): + _LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES, + devices) + return False + + dev_id_lookup = {} + + def device_tracker_message_received(topic, payload, qos): + """ MQTT message received. """ + see(dev_id=dev_id_lookup[topic], location_name=payload) + + for dev_id, topic in devices.items(): + dev_id_lookup[topic] = dev_id + mqtt.subscribe(hass, topic, device_tracker_message_received, qos) + + return True diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index c04a1d07b1f..46c515dcb1f 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -1,14 +1,13 @@ """ homeassistant.components.device_tracker.netgear ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Device tracker platform that supports scanning a Netgear router for device presence. Configuration: To use the Netgear tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: netgear @@ -42,7 +41,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear>=0.3'] +REQUIREMENTS = ['pynetgear==0.3'] def get_scanner(hass, config): @@ -71,7 +70,6 @@ class NetgearDeviceScanner(object): self.lock = threading.Lock() if host is None: - print("BIER") self._api = pynetgear.Netgear() elif username is None: self._api = pynetgear.Netgear(password, host) @@ -90,8 +88,9 @@ class NetgearDeviceScanner(object): _LOGGER.error("Failed to Login") def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() return (device.mac for device in self.last_results) @@ -106,8 +105,10 @@ class NetgearDeviceScanner(object): @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Retrieves latest information from the Netgear router. - Returns boolean if scanning successful. """ + """ + Retrieves latest information from the Netgear router. + Returns boolean if scanning successful. + """ if not self.success_init: return diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 00fc8fd12b4..5c619e001a3 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -1,13 +1,12 @@ """ homeassistant.components.device_tracker.nmap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Device tracker platform that supports scanning a network with nmap. Configuration: To use the nmap tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: nmap_tracker @@ -19,6 +18,11 @@ hosts *Required The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or the range notation (192.168.1.1-255). + +home_interval +*Optional +Number of minutes it will not scan devices that it found in previous results. +This is to save battery. """ import logging from datetime import timedelta @@ -26,9 +30,6 @@ from collections import namedtuple import subprocess import re -from libnmap.process import NmapProcess -from libnmap.parser import NmapParser, NmapParserException - import homeassistant.util.dt as dt_util from homeassistant.const import CONF_HOSTS from homeassistant.helpers import validate_config @@ -43,7 +44,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.3'] +REQUIREMENTS = ['python-nmap==0.4.1'] def get_scanner(hass, config): @@ -68,11 +69,11 @@ def _arp(ip_address): if match: return match.group(0) _LOGGER.info("No MAC address found for %s", ip_address) - return '' + return None class NmapDeviceScanner(object): - """ This class scans for devices using nmap """ + """ This class scans for devices using nmap. """ def __init__(self, config): self.last_results = [] @@ -81,13 +82,13 @@ class NmapDeviceScanner(object): minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) self.home_interval = timedelta(minutes=minutes) - self.success_init = True - self._update_info() + self.success_init = self._update_info() _LOGGER.info("nmap scanner initialized") def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() @@ -104,42 +105,17 @@ class NmapDeviceScanner(object): else: return None - def _parse_results(self, stdout): - """ Parses results from an nmap scan. - Returns True if successful, False otherwise. """ - try: - results = NmapParser.parse(stdout) - now = dt_util.now() - self.last_results = [] - for host in results.hosts: - if host.is_up(): - if host.hostnames: - name = host.hostnames[0] - else: - name = host.ipv4 - if host.mac: - mac = host.mac - else: - mac = _arp(host.ipv4) - if mac: - device = Device(mac.upper(), name, host.ipv4, now) - self.last_results.append(device) - _LOGGER.info("nmap scan successful") - return True - except NmapParserException as parse_exc: - _LOGGER.error("failed to parse nmap results: %s", parse_exc.msg) - self.last_results = [] - return False - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Scans the network for devices. - Returns boolean if scanning successful. """ - if not self.success_init: - return False - + """ + Scans the network for devices. + Returns boolean if scanning successful. + """ _LOGGER.info("Scanning") + from nmap import PortScanner, PortScannerError + scanner = PortScanner() + options = "-F --host-timeout 5" exclude_targets = set() if self.home_interval: @@ -151,14 +127,24 @@ class NmapDeviceScanner(object): target_list = [t.ip for t in exclude_targets] options += " --exclude {}".format(",".join(target_list)) - nmap = NmapProcess(targets=self.hosts, options=options) - - nmap.run() - - if nmap.rc == 0: - if self._parse_results(nmap.stdout): - self.last_results.extend(exclude_targets) - else: - self.last_results = [] - _LOGGER.error(nmap.stderr) + try: + result = scanner.scan(hosts=self.hosts, arguments=options) + except PortScannerError: return False + + now = dt_util.now() + self.last_results = [] + for ipv4, info in result['scan'].items(): + if info['status']['state'] != 'up': + continue + name = info['hostnames'][0] if info['hostnames'] else ipv4 + # Mac address only returned if nmap ran as root + mac = info['addresses'].get('mac') or _arp(ipv4) + if mac is None: + continue + device = Device(mac.upper(), name, ipv4, now) + self.last_results.append(device) + self.last_results.extend(exclude_targets) + + _LOGGER.info("nmap scan successful") + return True diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py new file mode 100644 index 00000000000..408daa94d81 --- /dev/null +++ b/homeassistant/components/device_tracker/thomson.py @@ -0,0 +1,160 @@ +""" +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 configuration.yaml file. + +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/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 1a189a08396..a23b7b80ff0 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -1,14 +1,13 @@ """ homeassistant.components.device_tracker.tomato ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Device tracker platform that supports scanning a Tomato router for device presence. Configuration: To use the Tomato tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: tomato diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 8e556e47e8a..6b12000cf45 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -1,14 +1,13 @@ """ homeassistant.components.device_tracker.tplink ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Device tracker platform that supports scanning a TP-Link router for device presence. Configuration: To use the TP-Link tracker you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. device_tracker: platform: tplink @@ -29,7 +28,6 @@ The username of an user with administrative privileges, usually 'admin'. password *Required The password for your given admin account. - """ import base64 import logging @@ -65,7 +63,8 @@ def get_scanner(hass, config): class TplinkDeviceScanner(object): - """ This class queries a wireless router running TP-Link firmware + """ + This class queries a wireless router running TP-Link firmware for connected devices. """ @@ -85,8 +84,9 @@ class TplinkDeviceScanner(object): self.success_init = self._update_info() def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() @@ -94,15 +94,18 @@ class TplinkDeviceScanner(object): # pylint: disable=no-self-use def get_device_name(self, device): - """ The TP-Link firmware doesn't save the name of the wireless - device. """ + """ + The TP-Link firmware doesn't save the name of the wireless device. + """ return None @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Ensures the information from the TP-Link router is up to date. - Returns boolean if scanning successful. """ + """ + Ensures the information from the TP-Link router is up to date. + Returns boolean if scanning successful. + """ with self.lock: _LOGGER.info("Loading wireless clients...") @@ -122,28 +125,33 @@ class TplinkDeviceScanner(object): class Tplink2DeviceScanner(TplinkDeviceScanner): - """ This class queries a wireless router running newer version of TP-Link + """ + This class queries a wireless router running newer version of TP-Link firmware for connected devices. """ def scan_devices(self): - """ Scans for new devices and return a - list containing found device ids. """ + """ + Scans for new devices and return a list containing found device ids. + """ self._update_info() return self.last_results.keys() # pylint: disable=no-self-use def get_device_name(self, device): - """ The TP-Link firmware doesn't save the name of the wireless - device. """ + """ + The TP-Link firmware doesn't save the name of the wireless device. + """ return self.last_results.get(device) @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Ensures the information from the TP-Link router is up to date. - Returns boolean if scanning successful. """ + """ + Ensures the information from the TP-Link router is up to date. + Returns boolean if scanning successful. + """ with self.lock: _LOGGER.info("Loading wireless clients...") diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 4ad0299cc8f..6a780693f25 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -19,22 +19,22 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['netdisco>=0.3'] +REQUIREMENTS = ['netdisco==0.4'] SCAN_INTERVAL = 300 # seconds -# Next 3 lines for now a mirror from netdisco.const -# Should setup a mapping netdisco.const -> own constants SERVICE_WEMO = 'belkin_wemo' SERVICE_HUE = 'philips_hue' SERVICE_CAST = 'google_cast' SERVICE_NETGEAR = 'netgear_router' +SERVICE_SONOS = 'sonos' SERVICE_HANDLERS = { SERVICE_WEMO: "switch", SERVICE_CAST: "media_player", SERVICE_HUE: "light", SERVICE_NETGEAR: 'device_tracker', + SERVICE_SONOS: 'media_player', } @@ -79,13 +79,6 @@ 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..8906e8902a0 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -5,24 +5,47 @@ Home Assistant - - - - - - + + + - - -

Initializing Home Assistant

- - - + + + +
+ +
Initializing
+
+ + + diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 3b00afea13b..41e727adf89 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 = "e9a79e5367c298e2e5201c35c9b9de8b" +VERSION = "397aa7c09f4938b1358672c9983f9f32" 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 26c38c2a31b..60831ab1e66 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -516,51 +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 1c82a536312..9637d5d2651 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 1c82a536312e8321716ab7d80a5d17045d20d77f +Subproject commit 9637d5d26516873b8a04a3c62b9596163c822a2d 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/group.py b/homeassistant/components/group.py index 1d307baaca9..09a3ff97634 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -104,7 +104,7 @@ def setup(hass, config): """ Sets up all groups found definded in the configuration. """ for name, entity_ids in config.get(DOMAIN, {}).items(): if isinstance(entity_ids, str): - entity_ids = entity_ids.split(",") + entity_ids = [ent.strip() for ent in entity_ids.split(",")] setup_group(hass, name, entity_ids) return True diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 1d2ccc9ab14..01f75eabb5a 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -147,7 +147,7 @@ def _api_history_period(handler, path_match, data): end_time = start_time + one_day - print("Fetchign", start_time, end_time) + print("Fetching", start_time, end_time) entity_id = data.get('filter_entity_id') diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index a28def8e7ba..8b2e2a6252c 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -205,9 +205,14 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): self.serve_forever() def register_path(self, method, url, callback, require_auth=True): - """ Registers a path wit the server. """ + """ Registers a path with the server. """ self.paths.append((method, url, callback, require_auth)) + def log_message(self, fmt, *args): + """ Redirect built-in log to HA logging """ + # pylint: disable=no-self-use + _LOGGER.info(fmt, *args) + # pylint: disable=too-many-public-methods,too-many-locals class RequestHandler(SimpleHTTPRequestHandler): @@ -225,6 +230,10 @@ class RequestHandler(SimpleHTTPRequestHandler): self._session = None SimpleHTTPRequestHandler.__init__(self, req, client_addr, server) + def log_message(self, fmt, *arguments): + """ Redirect built-in log to HA logging """ + _LOGGER.info(fmt, *arguments) + def _handle_request(self, method): # pylint: disable=too-many-branches """ Does some common checks and calls appropriate method. """ url = urlparse(self.path) @@ -478,7 +487,7 @@ class ServerSession: return self._expiry < date_util.utcnow() -class SessionStore: +class SessionStore(object): """ Responsible for storing and retrieving http sessions """ def __init__(self, enabled=True): """ Set up the session store """ diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py new file mode 100644 index 00000000000..1eacd61bcee --- /dev/null +++ b/homeassistant/components/ifttt.py @@ -0,0 +1,79 @@ +""" +homeassistant.components.ifttt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This component enable you to trigger Maker IFTTT recipes. +Check https://ifttt.com/maker for details. + +Configuration: + +To use Maker IFTTT you will need to add something like the following to your +config/configuration.yaml. + +ifttt: + key: xxxxx-x-xxxxxxxxxxxxx + +Variables: + +key +*Required +Your api key + +""" +import logging +import requests + +from homeassistant.helpers import validate_config + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "ifttt" + +SERVICE_TRIGGER = 'trigger' + +ATTR_EVENT = 'event' +ATTR_VALUE1 = 'value1' +ATTR_VALUE2 = 'value2' +ATTR_VALUE3 = 'value3' + +DEPENDENCIES = [] + +REQUIREMENTS = ['pyfttt==0.3'] + + +def trigger(hass, event, value1=None, value2=None, value3=None): + """ Trigger a Maker IFTTT recipe """ + data = { + ATTR_EVENT: event, + ATTR_VALUE1: value1, + ATTR_VALUE2: value2, + ATTR_VALUE3: value3, + } + hass.services.call(DOMAIN, SERVICE_TRIGGER, data) + + +def setup(hass, config): + """ Setup the ifttt service component """ + + if not validate_config(config, {DOMAIN: ['key']}, _LOGGER): + return False + + key = config[DOMAIN]['key'] + + def trigger_service(call): + """ Handle ifttt trigger service calls. """ + event = call.data.get(ATTR_EVENT) + value1 = call.data.get(ATTR_VALUE1) + value2 = call.data.get(ATTR_VALUE2) + value3 = call.data.get(ATTR_VALUE3) + if event is None: + return + + try: + import pyfttt as pyfttt + pyfttt.send_event(key, event, value1, value2, value3) + except requests.exceptions.RequestException: + _LOGGER.exception("Error communicating with IFTTT") + + hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service) + + return True diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py index b84a02d5fa5..3a1af572a30 100644 --- a/homeassistant/components/introduction.py +++ b/homeassistant/components/introduction.py @@ -29,8 +29,11 @@ def setup(hass, config=None): - Available components: https://home-assistant.io/components/ - - Chat room: - https://gitter.im/balloob/home-assistant + - Troubleshooting your configuration: + https://home-assistant.io/getting-started/troubleshooting-configuration.html + + - Getting help: + https://home-assistant.io/help/ This message is generated by the introduction component. You can disable it in configuration.yaml. 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/demo.py b/homeassistant/components/light/demo.py index 5c6b1ae6165..40a8cc023c5 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -12,9 +12,8 @@ from homeassistant.components.light import ( LIGHT_COLORS = [ - [0.861, 0.3259], - [0.6389, 0.3028], - [0.1684, 0.0416] + [0.368, 0.180], + [0.460, 0.470], ] @@ -22,8 +21,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return demo lights. """ add_devices_callback([ DemoLight("Bed Light", False), - DemoLight("Ceiling", True), - DemoLight("Kitchen", True) + DemoLight("Ceiling Lights", True, LIGHT_COLORS[0]), + DemoLight("Kitchen Lights", True, LIGHT_COLORS[1]) ]) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index c3b28ec1dd6..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) 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..9096bb32a10 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -15,7 +15,7 @@ Support for LimitlessLED bulbs, also known as... Configuration: To use limitlessled you will need to add the following to your -config/configuration.yaml. +configuration.yaml file. light: platform: limitlessled @@ -24,7 +24,6 @@ light: group_2_name: Bedroom group_3_name: Office group_4_name: Kitchen - """ import logging @@ -34,7 +33,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): @@ -107,7 +106,7 @@ class LimitlessLED(Light): return self._xy_color def _xy_to_led_color(self, xy_color): - """ Convert an XY color to the closest LedController color string """ + """ Convert an XY color to the closest LedController color string. """ def abs_dist_squared(p_0, p_1): """ Returns the absolute value of the squared distance """ return abs((p_0[0] - p_1[0])**2 + (p_0[1] - p_1[1])**2) 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/vera.py b/homeassistant/components/light/vera.py index f25c110cc46..f41bfb56685 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -1,7 +1,6 @@ """ homeassistant.components.light.vera ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Support for Vera lights. This component is useful if you wish for switches connected to your Vera controller to appear as lights in Home Assistant. All switches will be added as a light unless you exclude them in the config. @@ -9,17 +8,17 @@ All switches will be added as a light unless you exclude them in the config. Configuration: To use the Vera lights you will need to add something like the following to -your config/configuration.yaml. +your configuration.yaml file. light: - platform: vera - vera_controller_url: http://YOUR_VERA_IP:3480/ - device_data: - 12: - name: My awesome switch - exclude: true - 13: - name: Another switch + platform: vera + vera_controller_url: http://YOUR_VERA_IP:3480/ + device_data: + 12: + name: My awesome switch + exclude: true + 13: + name: Another switch Variables: @@ -52,8 +51,10 @@ it should be set to "true" if you want this device excluded. import logging from requests.exceptions import RequestException from homeassistant.components.switch.vera import VeraSwitch -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.vera.vera as veraApi + +REQUIREMENTS = ['https://github.com/balloob/home-assistant-vera-api/archive/' + 'a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip' + '#python-vera==0.1'] _LOGGER = logging.getLogger(__name__) @@ -61,6 +62,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Vera lights. """ + import pyvera as veraApi base_url = config.get('vera_controller_url') if not base_url: diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index e8c8eb7a224..98988c20688 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -9,8 +9,9 @@ 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' + '#python-wink==0.1'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 29bcb731062..f45dfe78054 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -25,6 +25,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DISCOVERY_PLATFORMS = { discovery.SERVICE_CAST: 'cast', + discovery.SERVICE_SONOS: 'sonos', } SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 5fca233013a..61223446e5f 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -1,10 +1,24 @@ """ homeassistant.components.media_player.chromecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to interact with Cast devices on the network. -WARNING: This platform is currently not working due to a changed Cast API +WARNING: This platform is currently not working due to a changed Cast API. + +Configuration: + +To use the chromecast integration you will need to add something like the +following to your configuration.yaml file. + +media_player: + platform: chromecast + host: 192.168.1.9 + +Variables: + +host +*Optional +Use only if you don't want to scan for devices. """ import logging @@ -19,7 +33,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.10'] +REQUIREMENTS = ['pychromecast==0.6.12'] 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 | \ diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 24f1e91c3ec..2a7bc5bde1b 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -1,9 +1,7 @@ """ homeassistant.components.media_player.demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Demo implementation of the media player. - """ from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_OFF) diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py new file mode 100644 index 00000000000..19286906f49 --- /dev/null +++ b/homeassistant/components/media_player/denon.py @@ -0,0 +1,191 @@ +""" +homeassistant.components.media_player.denon +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides an interface to Denon Network Receivers. +Developed for a Denon DRA-N5, see +http://www.denon.co.uk/chg/product/compactsystems/networkmusicsystems/ceolpiccolo + +A few notes: + - As long as this module is active and connected, the receiver does + not seem to accept additional telnet connections. + + - Be careful with the volume. 50% or even 100% are very loud. + + - To be able to wake up the receiver, activate the "remote" setting + in the receiver's settings. + + - Play and pause are supported, toggling is not possible. + + - Seeking cannot be implemented as the UI sends absolute positions. + Only seeking via simulated button presses is possible. + +Configuration: + +To use your Denon you will need to add something like the following to +your config/configuration.yaml: + +media_player: + platform: denon + name: Music station + host: 192.168.0.123 + +Variables: + +host +*Required +The ip of the player. Example: 192.168.0.123 + +name +*Optional +The name of the device. +""" +import telnetlib +import logging + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + DOMAIN) +from homeassistant.const import ( + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Denon platform. """ + if not config.get(CONF_HOST): + _LOGGER.error( + "Missing required configuration items in %s: %s", + DOMAIN, + CONF_HOST) + return False + + add_devices([ + DenonDevice( + config.get('name', 'Music station'), + config.get('host')) + ]) + + return True + + +class DenonDevice(MediaPlayerDevice): + """ Represents a Denon device. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, name, host): + self._name = name + self._host = host + self._telnet = telnetlib.Telnet(self._host) + + def query(self, message): + """ Send request and await response from server """ + try: + # unspecified command, should be ignored + self._telnet.write("?".encode('UTF-8') + b'\r') + except (EOFError, BrokenPipeError, ConnectionResetError): + self._telnet.open(self._host) + + self._telnet.read_very_eager() # skip what is not requested + + self._telnet.write(message.encode('ASCII') + b'\r') + # timeout 200ms, defined by protocol + resp = self._telnet.read_until(b'\r', timeout=0.2)\ + .decode('UTF-8').strip() + + if message == "PW?": + # workaround; PW? sends also SISTATUS + self._telnet.read_until(b'\r', timeout=0.2) + + return resp + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + pwstate = self.query('PW?') + if pwstate == "PWSTANDBY": + return STATE_OFF + if pwstate == "PWON": + return STATE_ON + + return STATE_UNKNOWN + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return int(self.query('MV?')[len('MV'):]) / 60 + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self.query('MU?') == "MUON" + + @property + def media_title(self): + """ Current media source. """ + return self.query('SI?')[len('SI'):] + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_DENON + + def turn_off(self): + """ turn_off media player. """ + self.query('PWSTANDBY') + + def volume_up(self): + """ volume_up media player. """ + self.query('MVUP') + + def volume_down(self): + """ volume_down media player. """ + self.query('MVDOWN') + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + # 60dB max + self.query('MV' + str(round(volume * 60)).zfill(2)) + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + self.query('MU' + ('ON' if mute else 'OFF')) + + def media_play_pause(self): + """ media_play_pause media player. """ + raise NotImplementedError() + + def media_play(self): + """ media_play media player. """ + self.query('NS9A') + + def media_pause(self): + """ media_pause media player. """ + self.query('NS9B') + + def media_next_track(self): + """ Send next track command. """ + self.query('NS9D') + + def media_previous_track(self): + self.query('NS9E') + + def media_seek(self, position): + raise NotImplementedError() + + def turn_on(self): + """ turn the media player on. """ + self.query('PWON') diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py new file mode 100644 index 00000000000..c1a22ec78f9 --- /dev/null +++ b/homeassistant/components/media_player/itunes.py @@ -0,0 +1,273 @@ +""" +homeassistant.components.media_player.itunes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides an interface to iTunes-API (https://github.com/maddox/itunes-api) + +Configuration: + +To use iTunes you will need to add something like the following to +your configuration.yaml file. + +media_player: + platform: itunes + name: iTunes + host: http://192.168.1.16 + port: 8181 + +Variables: + +name +*Optional +The name of the device. + +url +*Required +URL of your running version of iTunes-API. Example: http://192.168.1.50:8181 + +""" +import logging + +from homeassistant.components.media_player import ( + MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK) +from homeassistant.const import ( + STATE_IDLE, STATE_PLAYING, STATE_PAUSED) + +import requests + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + + +class Itunes(object): + """ itunes-api client. """ + + def __init__(self, host, port): + self.host = host + self.port = port + + @property + def _base_url(self): + """ Returns the base url for endpoints. """ + return self.host + ":" + str(self.port) + + def _request(self, method, path, params=None): + """ Makes the actual request and returns the parsed response. """ + url = self._base_url + path + + try: + if method == 'GET': + response = requests.get(url) + elif method == "POST": + response = requests.put(url, params) + elif method == "PUT": + response = requests.put(url, params) + elif method == "DELETE": + response = requests.delete(url) + + return response.json() + except requests.exceptions.HTTPError: + return {'player_state': 'error'} + except requests.exceptions.RequestException: + return {'player_state': 'offline'} + + def _command(self, named_command): + """ Makes a request for a controlling command. """ + return self._request('PUT', '/' + named_command) + + def now_playing(self): + """ Returns the current state. """ + return self._request('GET', '/now_playing') + + def set_volume(self, level): + """ Sets the volume and returns the current state, level 0-100. """ + return self._request('PUT', '/volume', {'level': level}) + + def set_muted(self, muted): + """ Mutes and returns the current state, muted True or False. """ + return self._request('PUT', '/mute', {'muted': muted}) + + def play(self): + """ Sets playback to play and returns the current state. """ + return self._command('play') + + def pause(self): + """ Sets playback to paused and returns the current state. """ + return self._command('pause') + + def next(self): + """ Skips to the next track and returns the current state. """ + return self._command('next') + + def previous(self): + """ Skips back and returns the current state. """ + return self._command('previous') + + def artwork_url(self): + """ Returns a URL of the current track's album art. """ + return self._base_url + '/artwork' + +# pylint: disable=unused-argument +# pylint: disable=abstract-method +# pylint: disable=too-many-instance-attributes + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the itunes platform. """ + + add_devices([ + ItunesDevice( + config.get('name', 'iTunes'), + config.get('host'), + config.get('port') + ) + ]) + + +class ItunesDevice(MediaPlayerDevice): + """ Represents a iTunes-API instance. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, name, host, port): + self._name = name + self._host = host + self._port = port + + self.client = Itunes(self._host, self._port) + + self.current_volume = None + self.muted = None + self.current_title = None + self.current_album = None + self.current_artist = None + self.current_playlist = None + self.content_id = None + + self.player_state = None + + self.update() + + def update_state(self, state_hash): + """ Update all the state properties with the passed in dictionary. """ + self.player_state = state_hash.get('player_state', None) + + self.current_volume = state_hash.get('volume', 0) + self.muted = state_hash.get('muted', None) + self.current_title = state_hash.get('name', None) + self.current_album = state_hash.get('album', None) + self.current_artist = state_hash.get('artist', None) + self.current_playlist = state_hash.get('playlist', None) + self.content_id = state_hash.get('id', None) + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + + if self.player_state == 'offline' or self.player_state is None: + return 'offline' + + if self.player_state == 'error': + return 'error' + + if self.player_state == 'stopped': + return STATE_IDLE + + if self.player_state == 'paused': + return STATE_PAUSED + else: + return STATE_PLAYING + + def update(self): + """ Retrieve latest state. """ + now_playing = self.client.now_playing() + self.update_state(now_playing) + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self.muted + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return self.current_volume/100.0 + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self.content_id + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_MUSIC + + @property + def media_image_url(self): + """ Image url of current playing media. """ + + if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \ + self.current_title is not None: + return self.client.artwork_url() + else: + return 'https://cloud.githubusercontent.com/assets/260/9829355' \ + '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' + + @property + def media_title(self): + """ Title of current playing media. """ + return self.current_title + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return self.current_artist + + @property + def media_album_name(self): + """ Album of current playing media. (Music track only) """ + return self.current_album + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_ITUNES + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + response = self.client.set_volume(int(volume * 100)) + self.update_state(response) + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + response = self.client.set_muted(mute) + self.update_state(response) + + def media_play(self): + """ media_play media player. """ + response = self.client.play() + self.update_state(response) + + def media_pause(self): + """ media_pause media player. """ + response = self.client.pause() + self.update_state(response) + + def media_next_track(self): + """ media_next media player. """ + response = self.client.next() + self.update_state(response) + + def media_previous_track(self): + """ media_previous media player. """ + response = self.client.previous() + self.update_state(response) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 4b24f8694ed..7bfd385f65b 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -6,7 +6,7 @@ Provides an interface to the XBMC/Kodi JSON-RPC API Configuration: To use the Kodi you will need to add something like the following to -your config/configuration.yaml. +your configuration.yaml file. media_player: platform: kodi @@ -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 @@ -74,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def _get_image_url(kodi_url): - """ Helper function that parses the thumbnail URLs used by Kodi """ + """ Helper function that parses the thumbnail URLs used by Kodi. """ url_components = urllib.parse.urlparse(kodi_url) if url_components.scheme == 'image': @@ -107,6 +107,7 @@ class KodiDevice(MediaPlayerDevice): try: return self._server.Player.GetActivePlayers() except jsonrpc_requests.jsonrpc.TransportError: + _LOGGER.exception('Unable to fetch kodi data') return None @property @@ -235,7 +236,7 @@ class KodiDevice(MediaPlayerDevice): self.update_ha_state() def _set_play_state(self, state): - """ Helper method for play/pause/toggle """ + """ Helper method for play/pause/toggle. """ players = self._get_players() if len(players) != 0: @@ -256,7 +257,7 @@ class KodiDevice(MediaPlayerDevice): self._set_play_state(False) def _goto(self, direction): - """ Helper method used for previous/next track """ + """ Helper method used for previous/next track. """ players = self._get_players() if len(players) != 0: diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 0239173f7cc..8cc22f9b982 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -1,19 +1,19 @@ """ homeassistant.components.media_player.mpd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to interact with a Music Player Daemon. Configuration: To use MPD you will need to add something like the following to your -config/configuration.yaml +configuration.yaml file. media_player: platform: mpd server: 127.0.0.1 port: 6600 location: bedroom + password: superSecretPassword123 Variables: @@ -28,6 +28,10 @@ Port of the Music Player Daemon, defaults to 6600. Example: 6600 location *Optional Location of your Music Player Daemon. + +password +*Optional +Password for your Music Player Daemon. """ import logging import socket @@ -48,7 +52,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 @@ -61,6 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): daemon = config.get('server', None) port = config.get('port', 6600) location = config.get('location', 'MPD') + password = config.get('password', None) global mpd # pylint: disable=invalid-name if mpd is None: @@ -71,6 +76,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: mpd_client = mpd.MPDClient() mpd_client.connect(daemon, port) + + if password is not None: + mpd_client.password(password) + mpd_client.close() mpd_client.disconnect() except socket.error: @@ -79,8 +88,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Please check your settings") return False + except mpd.CommandError as error: - add_devices([MpdDevice(daemon, port, location)]) + if "incorrect password" in str(error): + _LOGGER.error( + "MPD reported incorrect password. " + "Please check your password.") + + return False + else: + raise + + add_devices([MpdDevice(daemon, port, location, password)]) class MpdDevice(MediaPlayerDevice): @@ -89,10 +108,11 @@ class MpdDevice(MediaPlayerDevice): # MPD confuses pylint # pylint: disable=no-member, abstract-method - def __init__(self, server, port, location): + def __init__(self, server, port, location, password): self.server = server self.port = port self._name = location + self.password = password self.status = None self.currentsong = None @@ -107,6 +127,10 @@ class MpdDevice(MediaPlayerDevice): self.currentsong = self.client.currentsong() except mpd.ConnectionError: self.client.connect(self.server, self.port) + + if self.password is not None: + self.client.password(self.password) + self.status = self.client.status() self.currentsong = self.client.currentsong() @@ -189,11 +213,11 @@ class MpdDevice(MediaPlayerDevice): def media_play(self): """ Service to send the MPD the command for play/pause. """ - self.client.start() + self.client.pause(0) def media_pause(self): """ Service to send the MPD the command for play/pause. """ - self.client.pause() + self.client.pause(1) def media_next_track(self): """ Service to send the MPD the command for next track. """ diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py new file mode 100644 index 00000000000..3deacd84e10 --- /dev/null +++ b/homeassistant/components/media_player/sonos.py @@ -0,0 +1,197 @@ +""" +homeassistant.components.media_player.sonos +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides an interface to Sonos players (via SoCo) + +Configuration: + +To use SoCo, add something like this to your configuration: + +media_player: + platform: sonos +""" + +import logging +import datetime + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC) + +from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import ( + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) + + +REQUIREMENTS = ['SoCo==0.11.1'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Sonos platform. """ + import soco + + players = soco.discover() + if not players: + _LOGGER.warning('No Sonos speakers found. Disabling: %s', __name__) + return False + + add_devices(SonosDevice(hass, p) for p in players) + _LOGGER.info('Added %s Sonos speakers', len(players)) + + return True + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods +# pylint: disable=abstract-method +class SonosDevice(MediaPlayerDevice): + """ Represents a Sonos device. """ + + # pylint: disable=too-many-arguments + def __init__(self, hass, player): + super(SonosDevice, self).__init__() + self._player = player + self.update() + + track_utc_time_change( + hass, self.update_sonos, + second=range(0, 60, 5)) + + @property + def should_poll(self): + return False + + def update_sonos(self, now): + """ Updates state, called by track_utc_time_change """ + self.update_ha_state(True) + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if self._status == 'PAUSED_PLAYBACK': + return STATE_PAUSED + if self._status == 'PLAYING': + return STATE_PLAYING + if self._status == 'STOPPED': + return STATE_IDLE + return STATE_UNKNOWN + + def update(self): + """ Retrieve latest state. """ + self._name = self._player.get_speaker_info()['zone_name'].replace( + ' (R)', '').replace(' (L)', '') + self._status = self._player.get_current_transport_info().get( + 'current_transport_state') + self._trackinfo = self._player.get_current_track_info() + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return self._player.volume / 100.0 + + @property + def is_volume_muted(self): + return self._player.mute + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self._trackinfo.get('title', None) + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + dur = self._trackinfo.get('duration', '0:00') + + # If the speaker is playing from the "line-in" source, getting + # track metadata can return NOT_IMPLEMENTED, which breaks the + # volume logic below + if dur == 'NOT_IMPLEMENTED': + return None + + return sum(60 ** x[0] * int(x[1]) for x in + enumerate(reversed(dur.split(':')))) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + if 'album_art' in self._trackinfo: + return self._trackinfo['album_art'] + + @property + def media_title(self): + """ Title of current playing media. """ + if 'artist' in self._trackinfo and 'title' in self._trackinfo: + return '{artist} - {title}'.format( + artist=self._trackinfo['artist'], + title=self._trackinfo['title'] + ) + if 'title' in self._status: + return self._trackinfo['title'] + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_SONOS + + def turn_off(self): + """ turn_off media player. """ + self._player.pause() + + def volume_up(self): + """ volume_up media player. """ + self._player.volume += 1 + + def volume_down(self): + """ volume_down media player. """ + self._player.volume -= 1 + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + self._player.volume = str(int(volume * 100)) + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + self._player.mute = mute + + def media_play(self): + """ media_play media player. """ + self._player.play() + + def media_pause(self): + """ media_pause media player. """ + self._player.pause() + + def media_next_track(self): + """ Send next track command. """ + self._player.next() + + def media_previous_track(self): + """ Send next track command. """ + self._player.previous() + + def media_seek(self, position): + """ Send seek command. """ + self._player.seek(str(datetime.timedelta(seconds=int(position)))) + + def turn_on(self): + """ turn the media player on. """ + self._player.play() diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 8cbc087c50c..940aa890f3a 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -1,12 +1,12 @@ """ homeassistant.components.media_player.squeezebox ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides an interface to the Logitech SqueezeBox API Configuration: -To use SqueezeBox add something like this to your configuration: +To use SqueezeBox add something something like the following to your +configuration.yaml file. media_player: platform: squeezebox @@ -19,19 +19,19 @@ Variables: host *Required -The host name or address of the Logitech Media Server +The host name or address of the Logitech Media Server. port *Optional -Telnet port to Logitech Media Server, default 9090 +Telnet port to Logitech Media Server, default 9090. usermame *Optional -Username, if password protection is enabled +Username, if password protection is enabled. password *Optional -Password, if password protection is enabled +Password, if password protection is enabled. """ import logging @@ -91,7 +91,7 @@ class LogitechMediaServer(object): self.init_success = True if self.http_port else False def _get_http_port(self): - """ Get http port from media server, it is used to get cover art """ + """ Get http port from media server, it is used to get cover art. """ http_port = None try: http_port = self.query('pref', 'httpport', '?') @@ -111,7 +111,7 @@ class LogitechMediaServer(object): return def create_players(self): - """ Create a list of SqueezeBoxDevices connected to the LMS """ + """ Create a list of SqueezeBoxDevices connected to the LMS. """ players = [] count = self.query('player', 'count', '?') for index in range(0, int(count)): @@ -121,7 +121,7 @@ class LogitechMediaServer(object): return players def query(self, *parameters): - """ Send request and await response from server """ + """ Send request and await response from server. """ telnet = telnetlib.Telnet(self.host, self.port) if self._username and self._password: telnet.write('login {username} {password}\n'.format( @@ -138,7 +138,7 @@ class LogitechMediaServer(object): return urllib.parse.unquote(response) def get_player_status(self, player): - """ Get ithe status of a player """ + """ Get ithe status of a player. """ # (title) : Song title # Requested Information # a (artist): Artist name 'artist' @@ -195,7 +195,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): def update(self): """ Retrieve latest state. """ - self._status = self._lms.get_player_status(self._name) + self._status = self._lms.get_player_status(self._id) @property def volume_level(self): diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 0bd3b23c2f9..844e59ea189 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -1,17 +1,12 @@ """ homeassistant.components.modbus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Modbus component, using pymodbus (python3 branch) - -Configuration: - -To use the Forecast sensor you will need to add something like the -following to your config/configuration.yaml +Modbus component, using pymodbus (python3 branch). Configuration: To use the Modbus component you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. #Modbus TCP modbus: @@ -38,8 +33,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#pymodbus==1.2.0'] # Type of network MEDIUM = "type" diff --git a/homeassistant/components/mqtt.py b/homeassistant/components/mqtt.py index aa1a3167029..7c7a12c2ac3 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' @@ -75,21 +75,23 @@ CONF_KEEPALIVE = 'keepalive' CONF_USERNAME = 'username' CONF_PASSWORD = 'password' -ATTR_QOS = 'qos' ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' +ATTR_QOS = 'qos' -def publish(hass, topic, payload): +def publish(hass, topic, payload, qos=None): """ Send an MQTT message. """ data = { ATTR_TOPIC: topic, ATTR_PAYLOAD: payload, } + if qos is not None: + data[ATTR_QOS] = qos hass.services.call(DOMAIN, SERVICE_PUBLISH, data) -def subscribe(hass, topic, callback, qos=0): +def subscribe(hass, topic, callback, qos=DEFAULT_QOS): """ Subscribe to a topic. """ def mqtt_topic_subscriber(event): """ Match subscribed MQTT topic. """ @@ -141,9 +143,10 @@ def setup(hass, config): """ Handle MQTT publish service calls. """ msg_topic = call.data.get(ATTR_TOPIC) payload = call.data.get(ATTR_PAYLOAD) + qos = call.data.get(ATTR_QOS, DEFAULT_QOS) if msg_topic is None or payload is None: return - MQTT_CLIENT.publish(msg_topic, payload) + MQTT_CLIENT.publish(msg_topic, payload, qos) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_mqtt) @@ -177,9 +180,9 @@ class MQTT(object): # pragma: no cover self._mqttc.on_message = self._mqtt_on_message self._mqttc.connect(broker, port, keepalive) - def publish(self, topic, payload): + def publish(self, topic, payload, qos): """ Publish a MQTT message. """ - self._mqttc.publish(topic, payload) + self._mqttc.publish(topic, payload, qos) def unsubscribe(self, topic): """ Unsubscribe from topic. """ diff --git a/homeassistant/components/notify/file.py b/homeassistant/components/notify/file.py index 186bc53ca98..9c0beca14ac 100644 --- a/homeassistant/components/notify/file.py +++ b/homeassistant/components/notify/file.py @@ -1,13 +1,12 @@ """ homeassistant.components.notify.file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - File notification service. Configuration: To use the File notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: file diff --git a/homeassistant/components/notify/instapush.py b/homeassistant/components/notify/instapush.py index 531ef758e05..95ff0d41435 100644 --- a/homeassistant/components/notify/instapush.py +++ b/homeassistant/components/notify/instapush.py @@ -1,13 +1,12 @@ """ homeassistant.components.notify.instapush ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Instapush notification service. Configuration: To use the Instapush notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: instapush @@ -16,7 +15,7 @@ notify: event: YOUR_EVENT tracker: YOUR_TRACKER -VARIABLES: +Variables: api_key *Required @@ -50,7 +49,6 @@ curl -X POST \ https://api.instapush.im/v1/post Details for the API : https://instapush.im/developer/rest - """ import logging import json diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index db6c91d8fed..bf8fb2162a8 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -1,19 +1,18 @@ """ homeassistant.components.notify.nma ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - NMA (Notify My Android) notification service. Configuration: To use the NMA notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: nma api_key: YOUR_API_KEY -VARIABLES: +Variables: api_key *Required @@ -21,7 +20,6 @@ Enter the API key for NMA. Go to https://www.notifymyandroid.com and create a new API key to use with Home Assistant. Details for the API : https://www.notifymyandroid.com/api.jsp - """ import logging import xml.etree.ElementTree as ET diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 5e322cfc3b5..76eaf5c0c37 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -1,13 +1,12 @@ """ homeassistant.components.notify.pushbullet ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - PushBullet platform for notify component. Configuration: To use the PushBullet notifier you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. notify: platform: pushbullet @@ -28,11 +27,11 @@ 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): - """ Get the pushbullet notification service. """ + """ Get the PushBullet notification service. """ if not validate_config(config, {DOMAIN: [CONF_API_KEY]}, diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 1bc5e9ac9a3..c52e430ac9f 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -1,18 +1,17 @@ """ homeassistant.components.notify.pushover ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Pushover platform for notify component. Configuration: To use the Pushover notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: - platform: pushover - api_key: ABCDEFGHJKLMNOPQRSTUVXYZ - user_key: ABCDEFGHJKLMNOPQRSTUVXYZ + platform: pushover + api_key: ABCDEFGHJKLMNOPQRSTUVXYZ + user_key: ABCDEFGHJKLMNOPQRSTUVXYZ Variables: @@ -33,7 +32,6 @@ https://home-assistant.io/images/favicon-192x192.png user_key *Required To retrieve this value log into your account at https://pushover.net - """ import logging @@ -42,7 +40,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..bd3a2b71c0c 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -6,7 +6,7 @@ Slack platform for notify component. Configuration: To use the Slack notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: slack @@ -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/smtp.py b/homeassistant/components/notify/smtp.py index 998391c81b4..0530ac4072d 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -1,13 +1,12 @@ """ homeassistant.components.notify.mail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mail notification service. +Mail (SMTP) notification service. Configuration: To use the Mail notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: mail diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 6673ba23a08..5d246f2fd0d 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -1,13 +1,12 @@ """ homeassistant.components.notify.syslog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Syslog notification service. Configuration: To use the Syslog notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: syslog diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index e756be82014..1d72f6a262b 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -6,7 +6,7 @@ Jabber (XMPP) notification service. Configuration: To use the Jabber notifier you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. notify: platform: xmpp @@ -45,7 +45,7 @@ from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) -REQUIREMENTS = ['sleekxmpp>=1.3.1', 'dnspython3>=1.12.0'] +REQUIREMENTS = ['sleekxmpp==1.3.1', 'dnspython3==1.12.0'] def get_service(hass, config): diff --git a/homeassistant/components/scheduler/time.py b/homeassistant/components/scheduler/time.py index 9fec19fbe57..4d0280dfdf9 100644 --- a/homeassistant/components/scheduler/time.py +++ b/homeassistant/components/scheduler/time.py @@ -28,7 +28,7 @@ def create_event_listener(schedule, event_listener_data): service = event_listener_data['service'] (hour, minute, second) = [int(x) for x in - event_listener_data['time'].split(':')] + event_listener_data['time'].split(':', 3)] return TimeEventListener(schedule, service, hour, minute, second) diff --git a/homeassistant/components/sensor/arduino.py b/homeassistant/components/sensor/arduino.py index 4210a064d13..f6c44d3f60e 100644 --- a/homeassistant/components/sensor/arduino.py +++ b/homeassistant/components/sensor/arduino.py @@ -6,6 +6,9 @@ supported. Configuration: +To use the arduino sensor you will need to add something like the following +to your configuration.yaml file. + sensor: platform: arduino pins: @@ -46,7 +49,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Arduino platform. """ - # Verify that Arduino board is present + # Verify that the Arduino board is present if arduino.BOARD is None: _LOGGER.error('A connection has not been made to the Arduino board.') return False diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py new file mode 100644 index 00000000000..cfe88e0f0d6 --- /dev/null +++ b/homeassistant/components/sensor/arest.py @@ -0,0 +1,150 @@ +""" +homeassistant.components.sensor.arest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The arest sensor will consume an exposed aREST API of a device. + +Configuration: + +To use the arest sensor you will need to add something like the following +to your configuration.yaml file. + +sensor: + platform: arest + resource: http://IP_ADDRESS + monitored_variables: + - name: temperature + unit: '°C' + - name: humidity + unit: '%' + +Variables: + +resource: +*Required +IP address of the device that is exposing an aREST API. + +These are the variables for the monitored_variables array: + +name +*Required +The name of the variable you wish to monitor. + +unit +*Optional +Defines the units of measurement of the sensor, if any. + +Details for the API: http://arest.io + +Format of a default JSON response by aREST: +{ + "variables":{ + "temperature":21, + "humidity":89 + }, + "id":"device008", + "name":"Bedroom", + "connected":true +} +""" +import logging +from requests import get, exceptions +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the aREST sensor. """ + + resource = config.get('resource', None) + + try: + response = get(resource, timeout=10) + except exceptions.MissingSchema: + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// to your URL.") + return False + except exceptions.ConnectionError: + _LOGGER.error("No route to device. " + "Please check the IP address in the configuration file.") + return False + + rest = ArestData(resource) + + dev = [] + for variable in config['monitored_variables']: + if 'unit' not in variable: + variable['unit'] = ' ' + if variable['name'] not in response.json()['variables']: + _LOGGER.error('Variable: "%s" does not exist', variable['name']) + else: + dev.append(ArestSensor(rest, + response.json()['name'], + variable['name'], + variable['unit'])) + + add_devices(dev) + + +class ArestSensor(Entity): + """ Implements an aREST sensor. """ + + def __init__(self, rest, location, variable, unit_of_measurement): + self.rest = rest + self._name = '{} {}'.format(location.title(), variable.title()) + self._variable = variable + self._state = 'n/a' + self._unit_of_measurement = unit_of_measurement + self.update() + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit the value is expressed in. """ + return self._unit_of_measurement + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + def update(self): + """ Gets the latest data from aREST API and updates the state. """ + self.rest.update() + values = self.rest.data + + if 'error' in values: + self._state = values['error'] + else: + self._state = values[self._variable] + + +# pylint: disable=too-few-public-methods +class ArestData(object): + """ Class for handling the data retrieval. """ + + def __init__(self, resource): + self.resource = resource + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from aREST device. """ + try: + response = get(self.resource, timeout=10) + if 'error' in self.data: + del self.data['error'] + self.data = response.json()['variables'] + except exceptions.ConnectionError: + _LOGGER.error("No route to device. Is device offline?") + self.data['error'] = 'n/a' diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index e0ecbab6db5..60a8a9172b7 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.bitcoin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Bitcoin information service that uses blockchain.info and its online wallet. Configuration: @@ -12,7 +11,7 @@ check 'Enable Api Access'. You will get an email message from blockchain.info where you must authorize the API access. To use the Bitcoin sensor you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. sensor: platform: bitcoin @@ -71,7 +70,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 218860290b0..333a0564dfb 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -1,9 +1,7 @@ """ homeassistant.components.sensor.demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Demo platform that has two fake sensors. - +Demo platform that has a couple of fake sensors. """ from homeassistant.helpers.entity import Entity from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py new file mode 100644 index 00000000000..2ce0b12be38 --- /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 configuration.yaml file. + +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' + '#Adafruit_DHT==1.1.0'] + +_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/efergy.py b/homeassistant/components/sensor/efergy.py index 893c4403d71..aed685dce67 100644 --- a/homeassistant/components/sensor/efergy.py +++ b/homeassistant/components/sensor/efergy.py @@ -7,7 +7,7 @@ Monitors home energy use as measured by an efergy engage hub using its Configuration: To use the efergy sensor you will need to add something like the following -to your config/configuration.yaml +to your configuration.yaml file. sensor: platform: efergy @@ -61,7 +61,7 @@ SENSOR_TYPES = { def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the efergy sensor. """ + """ Sets up the Efergy sensor. """ app_token = config.get("app_token") if not app_token: _LOGGER.error( @@ -118,7 +118,7 @@ class EfergySensor(Entity): return self._unit_of_measurement def update(self): - """ Gets the efergy monitor data from the web service """ + """ Gets the Efergy monitor data from the web service. """ if self.type == 'instant_readings': url_string = _RESOURCE + 'getInstant?token=' + self.app_token response = get(url_string) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index a9783104cd8..2c966530967 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -1,12 +1,12 @@ """ homeassistant.components.sensor.forecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Forecast.io service. +Forecast.io weather service. Configuration: To use the Forecast sensor you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. sensor: platform: forecast @@ -37,11 +37,9 @@ monitored_conditions *Required An array specifying the conditions to monitor. -These are the variables for the monitored_conditions array: - -type +monitored_conditions *Required -The condition you wish to monitor, see the configuration example above for a +Conditions to monitor. See the configuration example above for a list of all available conditions to monitor. Details for the API : https://developer.forecast.io/docs/v2 @@ -49,7 +47,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 @@ -73,7 +71,7 @@ SENSOR_TYPES = { 'humidity': ['Humidity', '%'], 'pressure': ['Pressure', 'mBar'], 'visibility': ['Visibility', 'km'], - 'ozone': ['Ozone', ''], + 'ozone': ['Ozone', 'DU'], } # Return cached results if last scan was less then this time ago diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 7a242c044f7..c30f618f715 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.isy994 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Support for ISY994 sensors. """ import logging diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index c1cbf101dc2..ac2a5e444d1 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -4,8 +4,9 @@ homeassistant.components.modbus Support for Modbus sensors. Configuration: + To use the Modbus sensors you will need to add something like the following to -your config/configuration.yaml +your configuration.yaml file. sensor: platform: modbus @@ -47,7 +48,6 @@ Note: - Each named register will create an integer sensor. - Each named bit will create a boolean sensor. """ - import logging import homeassistant.components.modbus as modbus @@ -61,7 +61,7 @@ DEPENDENCIES = ['modbus'] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Read config and create Modbus devices """ + """ Read config and create Modbus devices. """ sensors = [] slave = config.get("slave", None) if modbus.TYPE == "serial" and not slave: @@ -97,7 +97,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ModbusSensor(Entity): # pylint: disable=too-many-arguments - """ Represents a Modbus Sensor """ + """ Represents a Modbus Sensor. """ def __init__(self, name, slave, register, bit=None, unit=None, coil=False): self._name = name @@ -113,8 +113,10 @@ class ModbusSensor(Entity): @property def should_poll(self): - """ We should poll, because slaves are not allowed to - initiate communication on Modbus networks""" + """ + We should poll, because slaves are not allowed to + initiate communication on Modbus networks. + """ return True @property diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index d5dc192e450..37540820fcc 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -13,6 +13,7 @@ sensor: platform: mqtt name: "MQTT Sensor" state_topic: "home/bedroom/temperature" + qos: 0 unit_of_measurement: "ºC" Variables: @@ -25,6 +26,10 @@ state_topic *Required The MQTT topic subscribed to receive sensor values. +qos +*Optional +The maximum QoS level of the state topic. Default is 0. + unit_of_measurement *Optional Defines the units of measurement of the sensor, if any. @@ -38,6 +43,7 @@ import homeassistant.components.mqtt as mqtt _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "MQTT Sensor" +DEFAULT_QOS = 0 DEPENDENCIES = ['mqtt'] @@ -54,16 +60,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): hass, config.get('name', DEFAULT_NAME), config.get('state_topic'), + config.get('qos', DEFAULT_QOS), config.get('unit_of_measurement'))]) +# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSensor(Entity): """ Represents a sensor that can be updated using MQTT """ - def __init__(self, hass, name, state_topic, unit_of_measurement): + def __init__(self, hass, name, state_topic, qos, unit_of_measurement): self._state = "-" self._hass = hass self._name = name self._state_topic = state_topic + self._qos = qos self._unit_of_measurement = unit_of_measurement def message_received(topic, payload, qos): @@ -71,7 +80,7 @@ class MqttSensor(Entity): self._state = payload self.update_ha_state() - mqtt.subscribe(hass, self._state_topic, message_received) + mqtt.subscribe(hass, self._state_topic, message_received, self._qos) @property def should_poll(self): diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index a626858db31..60e84059cad 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -1,13 +1,12 @@ """ homeassistant.components.sensor.mysensors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Support for MySensors sensors. Configuration: To use the MySensors sensor you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. sensor: platform: mysensors @@ -36,8 +35,9 @@ 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' + '#pymysensors==0.2'] 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..5ca292a599f 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -6,7 +6,7 @@ OpenWeatherMap (OWM) service. Configuration: To use the OpenWeatherMap sensor you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. sensor: platform: openweathermap @@ -33,7 +33,7 @@ forecast Enables the forecast. The default is to display the current conditions. monitored_conditions -*Optional +*Required Conditions to monitor. See the configuration example above for a list of all available conditions to monitor. @@ -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..4cb8a939d5e 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -4,10 +4,9 @@ homeassistant.components.sensor.rfxtrx Shows sensor values from RFXtrx sensors. Configuration: -To use the rfxtrx sensors you will need to add something like the following to -your config/configuration.yaml -Example: +To use the rfxtrx sensors you will need to add something like the following to +your configuration.yaml file. sensor: platform: rfxtrx @@ -26,8 +25,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#RFXtrx==0.15'] DATA_TYPES = OrderedDict([ ('Temperature', TEMP_CELCIUS), @@ -102,4 +101,5 @@ class RfxtrxSensor(Entity): @property def unit_of_measurement(self): + """ Unit this state is expressed in. """ return self._unit_of_measurement diff --git a/homeassistant/components/sensor/rpi_gpio.py b/homeassistant/components/sensor/rpi_gpio.py index c57cf31b397..e8482ea56ac 100644 --- a/homeassistant/components/sensor/rpi_gpio.py +++ b/homeassistant/components/sensor/rpi_gpio.py @@ -1,7 +1,7 @@ # -*- 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. @@ -35,11 +35,10 @@ 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: @@ -53,7 +52,7 @@ DEFAULT_VALUE_HIGH = "HIGH" DEFAULT_VALUE_LOW = "LOW" DEFAULT_BOUNCETIME = 50 -REQUIREMENTS = ['RPi.GPIO>=0.5.11'] +REQUIREMENTS = ['RPi.GPIO==0.5.11'] _LOGGER = logging.getLogger(__name__) @@ -119,12 +118,12 @@ class RPiGPIOSensor(Entity): @property def should_poll(self): - """ No polling needed """ + """ No polling needed. """ return False @property def name(self): - """ The name of the sensor """ + """ The name of the sensor. """ return self._name @property diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index cc37bd96b6f..b372e777478 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -1,13 +1,12 @@ """ homeassistant.components.sensor.sabnzbd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Monitors SABnzbd NZB client API Configuration: To use the SABnzbd sensor you will need to add something like the following to -your config/configuration.yaml +your configuration.yaml file. sensor: platform: sabnzbd @@ -27,11 +26,11 @@ Variables: base_url *Required This is the base URL of your SABnzbd instance including the port number if not -running on 80. Example: http://192.168.1.32:8124/ +running on 80, e.g. http://192.168.1.32:8124/ name *Optional -The name to use when displaying this SABnzbd instance +The name to use when displaying this SABnzbd instance. monitored_variables *Required @@ -44,17 +43,17 @@ type The variable you wish to monitor, see the configuration example above for a list of all available variables. """ - from homeassistant.util import Throttle from datetime import timedelta from homeassistant.helpers.entity import Entity -# pylint: disable=no-name-in-module, import-error -from homeassistant.external.nzbclients.sabnzbd import SabnzbdApi -from homeassistant.external.nzbclients.sabnzbd import SabnzbdApiException import logging +REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/' + 'archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip' + '#python-sabnzbd==0.1'] + SENSOR_TYPES = { 'current_status': ['Status', ''], 'speed': ['Speed', 'MB/s'], @@ -71,7 +70,9 @@ _THROTTLED_REFRESH = None # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the sensors. """ + """ Sets up the SABnzbd sensors. """ + from pysabnzbd import SabnzbdApi, SabnzbdApiException + api_key = config.get("api_key") base_url = config.get("base_url") name = config.get("name", "SABnzbd") @@ -105,7 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SabnzbdSensor(Entity): - """ A Sabnzbd sensor """ + """ Represents an SABnzbd sensor. """ def __init__(self, sensor_type, sabnzb_client, client_name): self._name = SENSOR_TYPES[sensor_type][0] @@ -132,6 +133,7 @@ class SabnzbdSensor(Entity): def refresh_sabnzbd_data(self): """ Calls the throttled SABnzbd refresh method. """ if _THROTTLED_REFRESH is not None: + from pysabnzbd import SabnzbdApiException try: _THROTTLED_REFRESH() except SabnzbdApiException: diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index bd2d336d8ed..9f0b90d729f 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -1,14 +1,13 @@ """ homeassistant.components.sensor.swiss_public_transport ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The Swiss public transport sensor will give you the next two departure times from a given location to another one. This sensor is limited to Switzerland. Configuration: To use the Swiss public transport sensor you will need to add something like -the following to your config/configuration.yaml +the following to your configuration.yaml file. sensor: platform: swiss_public_transport @@ -54,7 +53,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: for location in [config.get('from', None), config.get('to', None)]: # transport.opendata.ch doesn't play nice with requests.Session - result = get(_RESOURCE + 'locations?query=%s' % location) + result = get(_RESOURCE + 'locations?query=%s' % location, + timeout=10) journey.append(result.json()['stations'][0]['name']) except KeyError: _LOGGER.exception( @@ -116,8 +116,8 @@ class PublicTransportData(object): 'from=' + self.start + '&' + 'to=' + self.destination + '&' + 'fields[]=connections/from/departureTimestamp/&' + - 'fields[]=connections/') - + 'fields[]=connections/', + timeout=10) connections = response.json()['connections'][:2] try: diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 1d1bdb1f3b5..ff7e908ccf1 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -1,13 +1,12 @@ """ homeassistant.components.sensor.systemmonitor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Shows system monitor values such as: disk, memory and processor use +Shows system monitor values such as: disk, memory, and processor use. Configuration: To use the System monitor sensor you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. sensor: platform: systemmonitor @@ -66,7 +65,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..7ee0fc19a99 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -1,8 +1,7 @@ """ homeassistant.components.sensor.tellstick ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Shows sensor values from tellstick sensors. +Shows sensor values from Tellstick sensors. Possible config keys: @@ -35,7 +34,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..c7943f0cc06 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -4,10 +4,9 @@ homeassistant.components.sensor.temper Support for getting temperature from TEMPer devices. Configuration: -To use the temper sensors you will need to add something like the following to -your config/configuration.yaml -Example: +To use the temper sensors you will need to add something like the following to +your configuration.yaml file. sensor: platform: temper @@ -18,7 +17,9 @@ 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' + '#temperusb==1.2.3'] # pylint: disable=unused-argument diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index 149730d4b5b..77011c3afad 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -1,13 +1,12 @@ """ homeassistant.components.sensor.time_date ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Date and Time service. Configuration: To use the Date and Time sensor you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. sensor: platform: time_date diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index b9ed3ea4e9f..992a8838f3f 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -1,31 +1,30 @@ """ homeassistant.components.sensor.transmission ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Monitors Transmission BitTorrent client API +Monitors Transmission BitTorrent client API. Configuration: To use the Transmission sensor you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. sensor: - platform: transmission - name: Transmission - host: 192.168.1.26 - port: 9091 - username: YOUR_USERNAME - password: YOUR_PASSWORD - monitored_variables: - - type: 'current_status' - - type: 'download_speed' - - type: 'upload_speed' + platform: transmission + name: Transmission + host: 192.168.1.26 + port: 9091 + username: YOUR_USERNAME + password: YOUR_PASSWORD + monitored_variables: + - 'current_status' + - 'download_speed' + - 'upload_speed' Variables: host *Required -This is the IP address of your Transmission daemon. Example: 192.168.1.32 +This is the IP address of your Transmission daemon, e.g. 192.168.1.32 port *Optional @@ -33,11 +32,11 @@ The port your Transmission daemon uses, defaults to 9091. Example: 8080 username *Required -Your Transmission username +Your Transmission username. password *Required -Your Transmission password +Your Transmission password. name *Optional @@ -45,16 +44,9 @@ The name to use when displaying this Transmission instance. monitored_variables *Required -An array specifying the variables to monitor. - -These are the variables for the monitored_variables array: - -type -*Required -The variable you wish to monitor, see the configuration example above for a -list of all available variables. +Variables to monitor. See the configuration example above for a +list of all available variables to monitor. """ - from homeassistant.util import Throttle from datetime import timedelta from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD @@ -67,7 +59,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'], @@ -81,7 +73,7 @@ _THROTTLED_REFRESH = None # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the sensors. """ + """ Sets up the Transmission sensors. """ host = config.get(CONF_HOST) username = config.get(CONF_USERNAME, None) password = config.get(CONF_PASSWORD, None) @@ -110,11 +102,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] for variable in config['monitored_variables']: - if variable['type'] not in SENSOR_TYPES: - _LOGGER.error('Sensor type: "%s" does not exist', variable['type']) + if variable not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable) else: dev.append(TransmissionSensor( - variable['type'], transmission_api, name)) + variable, transmission_api, name)) add_devices(dev) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 89b2a1a2964..b02e3acdea0 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -1,13 +1,12 @@ """ homeassistant.components.sensor.vera ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Support for Vera sensors. Configuration: To use the Vera sensors you will need to add something like the following to -your config/configuration.yaml +your configuration.yaml file. sensor: platform: vera @@ -24,8 +23,7 @@ Variables: vera_controller_url *Required This is the base URL of your vera controller including the port number if not -running on 80 -Example: http://192.168.1.21:3480/ +running on 80, e.g. http://192.168.1.21:3480/ device_data @@ -33,7 +31,7 @@ device_data This contains an array additional device info for your Vera devices. It is not required and if not specified all sensors configured in your Vera controller will be added with default values. You should use the id of your vera device -as the key for the device within device_data +as the key for the device within device_data. These are the variables for the device_data array: @@ -41,14 +39,12 @@ name *Optional This parameter allows you to override the name of your Vera device in the HA interface, if not specified the value configured for the device in your Vera -will be used - +will be used. exclude *Optional -This parameter allows you to exclude the specified device from homeassistant, -it should be set to "true" if you want this device excluded - +This parameter allows you to exclude the specified device from Home Assistant, +it should be set to "true" if you want this device excluded. """ import logging from requests.exceptions import RequestException @@ -58,8 +54,10 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT) -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.vera.vera as veraApi + +REQUIREMENTS = ['https://github.com/balloob/home-assistant-vera-api/archive/' + 'a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip' + '#python-vera==0.1'] _LOGGER = logging.getLogger(__name__) @@ -67,6 +65,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def get_devices(hass, config): """ Find and return Vera Sensors. """ + import pyvera as veraApi base_url = config.get('vera_controller_url') if not base_url: diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 4056bbd7733..39c8ce4671f 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -8,8 +8,9 @@ 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' + '#python-wink==0.1'] 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/__init__.py b/homeassistant/components/switch/__init__.py index 424d4505d39..b6dd31b48c2 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -24,6 +24,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_TODAY_MWH = "today_mwh" ATTR_CURRENT_POWER_MWH = "current_power_mwh" +ATTR_SENSOR_STATE = "sensor_state" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -38,6 +39,7 @@ DISCOVERY_PLATFORMS = { PROP_TO_ATTR = { 'current_power_mwh': ATTR_CURRENT_POWER_MWH, 'today_power_mw': ATTR_TODAY_MWH, + 'sensor_state': ATTR_SENSOR_STATE } _LOGGER = logging.getLogger(__name__) @@ -101,6 +103,16 @@ class SwitchDevice(ToggleEntity): """ Today total power usage in mw. """ return None + @property + def is_standby(self): + """ Is the device in standby. """ + return None + + @property + def sensor_state(self): + """ Is the sensor on or off. """ + return None + @property def device_state_attributes(self): """ Returns device specific state attributes. """ diff --git a/homeassistant/components/switch/arduino.py b/homeassistant/components/switch/arduino.py index 367e7378b27..b38cc290b23 100644 --- a/homeassistant/components/switch/arduino.py +++ b/homeassistant/components/switch/arduino.py @@ -1,11 +1,14 @@ """ homeassistant.components.switch.arduino ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Support for switching Arduino pins on and off. So fare only digital pins are +Support for switching Arduino pins on and off. So far only digital pins are supported. Configuration: +To use the arduino switch you will need to add something like the following +to your configuration.yaml file. + switch: platform: arduino pins: @@ -62,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ArduinoSwitch(SwitchDevice): - """ Represents an Arduino Switch. """ + """ Represents an Arduino switch. """ def __init__(self, name, pin, pin_type): self._pin = pin self._name = name or DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py new file mode 100644 index 00000000000..239e24a4925 --- /dev/null +++ b/homeassistant/components/switch/arest.py @@ -0,0 +1,123 @@ +""" +homeassistant.components.switch.arest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The arest switch can control the digital pins of a device running with the +aREST RESTful framework for Arduino, the ESP8266, and the Raspberry Pi. +Only tested with Arduino boards so far. + +Configuration: + +To use the arest switch you will need to add something like the following +to your configuration.yaml file. + +sensor: + platform: arest + resource: http://IP_ADDRESS + pins: + 11: + name: Fan Office + 12: + name: Light Desk + +Variables: + +resource: +*Required +IP address of the device that is exposing an aREST API. + +pins: +The number of the digital pin to switch. + +These are the variables for the pins array: + +name +*Required +The name for the pin that will be used in the frontend. + +Details for the API: http://arest.io +""" +import logging +from requests import get, exceptions + +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import DEVICE_DEFAULT_NAME + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the aREST switches. """ + + resource = config.get('resource', None) + + try: + response = get(resource, timeout=10) + except exceptions.MissingSchema: + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// to your URL.") + return False + except exceptions.ConnectionError: + _LOGGER.error("No route to device. " + "Please check the IP address in the configuration file.") + return False + + dev = [] + pins = config.get('pins') + for pinnum, pin in pins.items(): + dev.append(ArestSwitch(resource, + response.json()['name'], + pin.get('name'), + pinnum)) + add_devices(dev) + + +class ArestSwitch(SwitchDevice): + """ Implements an aREST switch. """ + + def __init__(self, resource, location, name, pin): + self._resource = resource + self._name = '{} {}'.format(location.title(), name.title()) \ + or DEVICE_DEFAULT_NAME + self._pin = pin + self._state = None + + request = get('{}/mode/{}/o'.format(self._resource, self._pin), + timeout=10) + if request.status_code is not 200: + _LOGGER.error("Can't set mode. Is device offline?") + + @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. """ + request = get('{}/digital/{}/1'.format(self._resource, self._pin), + timeout=10) + if request.status_code == 200: + self._state = True + else: + _LOGGER.error("Can't turn on pin %s at %s. Is device offline?", + self._resource, self._pin) + + def turn_off(self, **kwargs): + """ Turn the device off. """ + request = get('{}/digital/{}/0'.format(self._resource, self._pin), + timeout=10) + if request.status_code == 200: + self._state = False + else: + _LOGGER.error("Can't turn off pin %s at %s. Is device offline?", + self._resource, self._pin) + + def update(self): + """ Gets the latest data from aREST API and updates the state. """ + request = get('{}/digital/{}'.format(self._resource, self._pin), + timeout=10) + self._state = request.json()['return_value'] != 0 diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 7cc51a1f9b9..4d5aeba94f5 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ homeassistant.components.switch.command_switch -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows to configure custom shell commands to turn a switch on/off. """ @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class CommandSwitch(SwitchDevice): - """ Represents a switch that can be togggled using shell commands """ + """ Represents a switch that can be togggled using shell commands. """ def __init__(self, name, command_on, command_off): self._name = name self._state = False @@ -39,7 +39,7 @@ class CommandSwitch(SwitchDevice): @staticmethod def _switch(command): - """ Execute the actual commands """ + """ Execute the actual commands. """ _LOGGER.info('Running command: %s', command) success = (subprocess.call(command, shell=True) == 0) @@ -51,12 +51,12 @@ class CommandSwitch(SwitchDevice): @property def should_poll(self): - """ No polling needed """ + """ No polling needed. """ return False @property def name(self): - """ The name of the switch """ + """ The name of the switch. """ return self._name @property diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py index 7b2f077d6a8..4ee1dc82413 100644 --- a/homeassistant/components/switch/demo.py +++ b/homeassistant/components/switch/demo.py @@ -3,7 +3,6 @@ homeassistant.components.switch.demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Demo platform that has two fake switches. - """ from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME @@ -13,7 +12,7 @@ from homeassistant.const import DEVICE_DEFAULT_NAME def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return demo switches. """ add_devices_callback([ - DemoSwitch('Ceiling', True), + DemoSwitch('Decorative Lights', True), DemoSwitch('AC', False) ]) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 17fe6d61735..2f38084ed9d 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -6,7 +6,7 @@ Support for Edimax switches. Configuration: To use the Edimax switch you will need to add something like the following to -your config/configuration.yaml. +your configuration.yaml file. switch: platform: edimax @@ -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#pyedimax==0.1'] # setup logger _LOGGER = logging.getLogger(__name__) @@ -88,7 +89,7 @@ class SmartPlugSwitch(SwitchDevice): @property def current_power_mwh(self): - """ Current power usage in mwh. """ + """ Current power usage in mWh. """ try: return float(self.smartplug.now_power) / 1000000.0 except ValueError: @@ -96,7 +97,7 @@ class SmartPlugSwitch(SwitchDevice): @property def today_power_mw(self): - """ Today total power usage in mw. """ + """ Today total power usage in mW. """ try: return float(self.smartplug.now_energy_day) / 1000.0 except ValueError: 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/modbus.py b/homeassistant/components/switch/modbus.py index dbf9bc83aad..5a00b9cb174 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -1,12 +1,12 @@ """ homeassistant.components.switch.modbus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Support for Modbus switches. Configuration: + To use the Modbus switches you will need to add something like the following to -your config/configuration.yaml +your configuration.yaml file. sensor: platform: modbus @@ -42,7 +42,7 @@ DEPENDENCIES = ['modbus'] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Read config and create Modbus devices """ + """ Read configuration and create Modbus devices. """ switches = [] slave = config.get("slave", None) if modbus.TYPE == "serial" and not slave: @@ -87,8 +87,10 @@ class ModbusSwitch(ToggleEntity): @property def should_poll(self): - """ We should poll, because slaves are not allowed to - initiate communication on Modbus networks""" + """ + We should poll, because slaves are not allowed to initiate + communication on Modbus networks. + """ return True @property diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index c6ebdaa2ad6..73618bd9277 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -26,6 +26,7 @@ switch: name: "Bedroom Switch" state_topic: "home/bedroom/switch1" command_topic: "home/bedroom/switch1/set" + qos: 0 payload_on: "ON" payload_off: "OFF" optimistic: false @@ -45,6 +46,11 @@ command_topic *Required The MQTT topic to publish commands to change the switch state. +qos +*Optional +The maximum QoS level of the state topic. Default is 0. +This QoS will also be used to publishing messages. + payload_on *Optional The payload that represents enabled state. Default is "ON". @@ -66,6 +72,7 @@ from homeassistant.components.switch import SwitchDevice _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "MQTT Switch" +DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_OPTIMISTIC = False @@ -86,6 +93,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config.get('name', DEFAULT_NAME), config.get('state_topic'), config.get('command_topic'), + config.get('qos', DEFAULT_QOS), config.get('payload_on', DEFAULT_PAYLOAD_ON), config.get('payload_off', DEFAULT_PAYLOAD_OFF), config.get('optimistic', DEFAULT_OPTIMISTIC))]) @@ -94,13 +102,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # 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, + def __init__(self, hass, name, state_topic, command_topic, qos, 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._qos = qos self._payload_on = payload_on self._payload_off = payload_off self._optimistic = optimistic @@ -119,7 +128,8 @@ class MqttSwitch(SwitchDevice): self._optimistic = True else: # subscribe the state_topic - mqtt.subscribe(hass, self._state_topic, message_received) + mqtt.subscribe(hass, self._state_topic, message_received, + self._qos) @property def should_poll(self): @@ -138,7 +148,8 @@ class MqttSwitch(SwitchDevice): def turn_on(self, **kwargs): """ Turn the device on. """ - mqtt.publish(self.hass, self._command_topic, self._payload_on) + mqtt.publish(self.hass, self._command_topic, self._payload_on, + self._qos) if self._optimistic: # optimistically assume that switch has changed state self._state = True @@ -146,7 +157,8 @@ class MqttSwitch(SwitchDevice): def turn_off(self, **kwargs): """ Turn the device off. """ - mqtt.publish(self.hass, self._command_topic, self._payload_off) + mqtt.publish(self.hass, self._command_topic, self._payload_off, + self._qos) if self._optimistic: # optimistically assume that switch has changed state self._state = False diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index bb9cf13e3ed..08c7ff35255 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -2,10 +2,14 @@ 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: +To use the Raspberry GPIO switches you will need to add something like the +following to your configuration.yaml file. + switch: platform: rpi_gpio invert_logic: false @@ -36,7 +40,7 @@ from homeassistant.const import (DEVICE_DEFAULT_NAME, DEFAULT_INVERT_LOGIC = False -REQUIREMENTS = ['RPi.GPIO>=0.5.11'] +REQUIREMENTS = ['RPi.GPIO==0.5.11'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 4fde4babf9e..ae064d4fdb8 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -1,7 +1,6 @@ """ homeassistant.components.switch.tellstick ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Support for Tellstick switches. Because the tellstick sends its actions via radio and from most @@ -19,7 +18,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 @@ -47,7 +46,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TellstickSwitchDevice(ToggleEntity): - """ Represents a Tellstick switch within Home Assistant. """ + """ Represents a Tellstick switch. """ last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | tellcore_constants.TELLSTICK_TURNOFF) diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index 7575951f53b..8288ed8456b 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -6,15 +6,15 @@ Enable or disable Transmission BitTorrent client Turtle Mode. Configuration: To use the Transmission switch you will need to add something like the -following to your config/configuration.yaml +following to your configuration.yaml file. switch: - platform: transmission - name: Transmission - host: 192.168.1.26 - port: 9091 - username: YOUR_USERNAME - password: YOUR_PASSWORD + platform: transmission + name: Transmission + host: 192.168.1.26 + port: 9091 + username: YOUR_USERNAME + password: YOUR_PASSWORD Variables: @@ -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/vera.py b/homeassistant/components/switch/vera.py index 77b49ebf826..bb7f43522f4 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -5,7 +5,7 @@ Support for Vera switches. Configuration: To use the Vera lights you will need to add something like the following to -your config/configuration.yaml. +your configuration.yaml file. switch: platform: vera @@ -52,8 +52,10 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.vera.vera as veraApi + +REQUIREMENTS = ['https://github.com/balloob/home-assistant-vera-api/archive/' + 'a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip' + '#python-vera==0.1'] _LOGGER = logging.getLogger(__name__) @@ -61,6 +63,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def get_devices(hass, config): """ Find and return Vera switches. """ + import pyvera as veraApi base_url = config.get('vera_controller_url') if not base_url: diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index 6c8f0352c3f..7840defdfc4 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -1,7 +1,7 @@ """ homeassistant.components.switch.verisure -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Support for Verisure Smartplugs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Verisure Smartplugs. """ import logging @@ -12,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Arduino platform. """ + """ Sets up the Verisure platform. """ if not verisure.MY_PAGES: _LOGGER.error('A connection has not been made to Verisure mypages.') @@ -48,13 +48,13 @@ class VerisureSmartplug(SwitchDevice): return plug_status == self.status_on def turn_on(self): - """ Set smartplug status on """ + """ Set smartplug status on. """ verisure.MY_PAGES.set_smartplug_status( self._id, self.status_on) def turn_off(self): - """ Set smartplug status off """ + """ Set smartplug status off. """ verisure.MY_PAGES.set_smartplug_status( self._id, self.status_off) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index d133191e6db..1a78a7d6725 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -7,8 +7,9 @@ Support for WeMo switches. import logging from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY -REQUIREMENTS = ['pywemo>=0.2'] +REQUIREMENTS = ['pywemo==0.3'] # pylint: disable=unused-argument @@ -35,10 +36,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class WemoSwitch(SwitchDevice): - """ Represents a WeMo switch within Home Assistant. """ + """ Represents a WeMo switch. """ def __init__(self, wemo): self.wemo = wemo self.insight_params = None + self.maker_params = None @property def unique_id(self): @@ -50,6 +52,16 @@ class WemoSwitch(SwitchDevice): """ Returns the name of the switch if any. """ return self.wemo.name + @property + def state(self): + """ Returns the state. """ + is_on = self.is_on + if not is_on: + return STATE_OFF + elif self.is_standby: + return STATE_STANDBY + return STATE_ON + @property def current_power_mwh(self): """ Current power usage in mwh. """ @@ -62,6 +74,40 @@ class WemoSwitch(SwitchDevice): if self.insight_params: return self.insight_params['todaymw'] + @property + def is_standby(self): + """ Is the device on - or in standby. """ + if self.insight_params: + standby_state = self.insight_params['state'] + # Standby is actually '8' but seems more defensive + # to check for the On and Off states + if standby_state == '1' or standby_state == '0': + return False + else: + return True + + @property + def sensor_state(self): + """ Is the sensor on or off. """ + if self.maker_params and self.has_sensor: + # Note a state of 1 matches the WeMo app 'not triggered'! + if self.maker_params['sensorstate']: + return STATE_OFF + else: + return STATE_ON + + @property + def switch_mode(self): + """ Is the switch configured as toggle(0) or momentary (1). """ + if self.maker_params: + return self.maker_params['switchmode'] + + @property + def has_sensor(self): + """ Is the sensor present? """ + if self.maker_params: + return self.maker_params['hassensor'] + @property def is_on(self): """ True if switch is on. """ @@ -78,5 +124,8 @@ class WemoSwitch(SwitchDevice): def update(self): """ Update WeMo state. """ self.wemo.get_state(True) - if self.wemo.model.startswith('Belkin Insight'): + if self.wemo.model_name == 'Insight': self.insight_params = self.wemo.insight_params + self.insight_params['standby_state'] = self.wemo.get_standby_state + elif self.wemo.model_name == 'Maker': + self.maker_params = self.wemo.maker_params diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 556a40b181f..da708d2a1f7 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -9,8 +9,9 @@ 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' + '#python-wink==0.1'] 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 bbc0979e38c..e9d3c50451b 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -23,10 +23,17 @@ SCAN_INTERVAL = 60 SERVICE_SET_AWAY_MODE = "set_away_mode" SERVICE_SET_TEMPERATURE = "set_temperature" +STATE_HEAT = "heat" +STATE_COOL = "cool" +STATE_IDLE = "idle" + ATTR_CURRENT_TEMPERATURE = "current_temperature" ATTR_AWAY_MODE = "away_mode" ATTR_MAX_TEMP = "max_temp" ATTR_MIN_TEMP = "min_temp" +ATTR_TEMPERATURE_LOW = "target_temp_low" +ATTR_TEMPERATURE_HIGH = "target_temp_high" +ATTR_OPERATION = "current_operation" _LOGGER = logging.getLogger(__name__) @@ -126,19 +133,25 @@ class ThermostatDevice(Entity): user_unit = self.hass.config.temperature_unit data = { - 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) + 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), + ATTR_TEMPERATURE: round(convert( + self.target_temperature, thermostat_unit, user_unit), 0), + ATTR_TEMPERATURE_LOW: round(convert( + self.target_temperature_low, thermostat_unit, user_unit), 0), + ATTR_TEMPERATURE_HIGH: round(convert( + self.target_temperature_high, thermostat_unit, user_unit), 0), } - is_away = self.is_away_mode_on + operation = self.operation + if operation is not None: + data[ATTR_OPERATION] = operation + is_away = self.is_away_mode_on if is_away is not None: data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF @@ -152,18 +165,33 @@ class ThermostatDevice(Entity): @property def unit_of_measurement(self): """ Unit of measurement this thermostat expresses itself in. """ - return NotImplementedError + raise NotImplementedError @property def current_temperature(self): """ Returns the current temperature. """ raise NotImplementedError + @property + def operation(self): + """ Returns current operation ie. heat, cool, idle """ + return None + @property def target_temperature(self): """ Returns the temperature we try to reach. """ raise NotImplementedError + @property + def target_temperature_low(self): + """ Returns the lower bound temperature we try to reach. """ + return self.target_temperature + + @property + def target_temperature_high(self): + """ Returns the upper bound temperature we try to reach. """ + return self.target_temperature + @property def is_away_mode_on(self): """ diff --git a/homeassistant/components/thermostat/demo.py b/homeassistant/components/thermostat/demo.py index 0ecbd889edd..9ad9e8995cd 100644 --- a/homeassistant/components/thermostat/demo.py +++ b/homeassistant/components/thermostat/demo.py @@ -3,7 +3,6 @@ homeassistant.components.thermostat.demo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Demo platform that offers a fake thermostat. - """ from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import TEMP_CELCIUS, TEMP_FAHRENHEIT @@ -19,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments class DemoThermostat(ThermostatDevice): - """ Represents a HeatControl within Home Assistant. """ + """ Represents a HeatControl thermostat. """ def __init__(self, name, target_temperature, unit_of_measurement, away, current_temperature): @@ -60,7 +59,7 @@ class DemoThermostat(ThermostatDevice): return self._away def set_temperature(self, temperature): - """ Set new target temperature """ + """ Set new target temperature. """ self._target_temperature = temperature def turn_away_mode_on(self): diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 7eb3030f0d4..f77d4285544 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -1,4 +1,6 @@ """ +homeassistant.components.thermostat.heat_control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Adds support for a thermostat. Specify a start time, end time and a target temperature. @@ -12,8 +14,8 @@ temperature (min_temp in config). The min temperature is also used as target temperature when no other temperature is specified. If the heater is manually turned on, the target temperature will -be sat to 100*C. Meaning -the thermostat probably will never turn off the heater. +be sat to 100*C. Meaning the thermostat probably will never turn +off the heater. If the heater is manually turned off, the target temperature will be sat according to normal rules. (Based on target temperature for given time intervals and the min temperature.) @@ -21,7 +23,6 @@ for given time intervals and the min temperature.) A target temperature sat with the set_temperature function will override all other rules for the target temperature. - Config: [thermostat] @@ -55,9 +56,7 @@ target temperature will be 17*C. Between 0745 and 1500 no temperature is specified. so the min_temp of 10*C will be used. From 1500 to 1850 the target temperature is 20*, but if away mode is true the target temperature will be sat to 10*C - """ - import logging import datetime import homeassistant.components as core @@ -80,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes class HeatControl(ThermostatDevice): - """ Represents a HeatControl within Home Assistant. """ + """ Represents a HeatControl device. """ def __init__(self, hass, config, logger): @@ -150,14 +149,14 @@ class HeatControl(ThermostatDevice): return self.min_temp def set_temperature(self, temperature): - """ Set new target temperature """ + """ Set new target temperature. """ if temperature is None: self._manual_sat_temp = None else: self._manual_sat_temp = float(temperature) def update(self): - """ Update current thermostat """ + """ Update current thermostat. """ heater = self.hass.states.get(self.heater_entity_id) if heater is None: self.logger.error("No heater available") @@ -178,7 +177,7 @@ class HeatControl(ThermostatDevice): core.turn_on(self.hass, self.heater_entity_id) def _heater_turned_on(self, entity_id, old_state, new_state): - """ heater is turned on""" + """ Heater is turned on. """ if not self._heater_manual_changed: pass else: @@ -187,7 +186,7 @@ class HeatControl(ThermostatDevice): self._heater_manual_changed = True def _heater_turned_off(self, entity_id, old_state, new_state): - """ heater is turned off""" + """ Heater is turned off. """ if self._heater_manual_changed: self.set_temperature(None) diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index cb74fa091ff..656becd6a21 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -1,12 +1,16 @@ """ +homeassistant.components.thermostat.nest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Adds support for Nest thermostats. """ +import socket import logging -from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, + STATE_IDLE, STATE_HEAT) from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) -REQUIREMENTS = ['python-nest>=2.4.0'] +REQUIREMENTS = ['python-nest==2.6.0'] # pylint: disable=unused-argument @@ -32,16 +36,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return napi = nest.Nest(username, password) - - add_devices([ - NestThermostat(structure, device) - for structure in napi.structures - for device in structure.devices - ]) + try: + add_devices([ + NestThermostat(structure, device) + for structure in napi.structures + for device in structure.devices + ]) + except socket.error: + logger.error( + "Connection error logging into the nest web service" + ) class NestThermostat(ThermostatDevice): - """ Represents a Nest thermostat within Home Assistant. """ + """ Represents a Nest thermostat. """ def __init__(self, structure, device): self.structure = structure @@ -81,25 +89,52 @@ class NestThermostat(ThermostatDevice): """ Returns the current temperature. """ return round(self.device.temperature, 1) + @property + def operation(self): + """ Returns current operation ie. heat, cool, idle """ + if self.device.hvac_ac_state is True: + return STATE_COOL + elif self.device.hvac_heater_state is True: + return STATE_HEAT + else: + return STATE_IDLE + @property def target_temperature(self): """ Returns the temperature we try to reach. """ target = self.device.target - if isinstance(target, tuple): + if self.device.mode == 'range': low, high = target - - if self.current_temperature < low: - temp = low - elif self.current_temperature > high: + if self.operation == STATE_COOL: temp = high + elif self.operation == STATE_HEAT: + temp = low else: - temp = (low + high)/2 + range_average = (low + high)/2 + if self.current_temperature < range_average: + temp = low + elif self.current_temperature >= range_average: + temp = high else: temp = target return round(temp, 1) + @property + def target_temperature_low(self): + """ Returns the lower bound temperature we try to reach. """ + if self.device.mode == 'range': + return round(self.device.target[0], 1) + return round(self.target_temperature, 1) + + @property + def target_temperature_high(self): + """ Returns the upper bound temperature we try to reach. """ + if self.device.mode == 'range': + return round(self.device.target[1], 1) + return round(self.target_temperature, 1) + @property def is_away_mode_on(self): """ Returns if away mode is on. """ @@ -107,6 +142,11 @@ class NestThermostat(ThermostatDevice): def set_temperature(self, temperature): """ Set new target temperature """ + if self.device.mode == 'range': + if self.target_temperature == self.target_temperature_low: + temperature = (temperature, self.target_temperature_high) + elif self.target_temperature == self.target_temperature_high: + temperature = (self.target_temperature_low, temperature) self.device.target = temperature def turn_away_mode_on(self): diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 4e97cca0cd4..5024efab6e6 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -1,11 +1,13 @@ """ components.verisure -~~~~~~~~~~~~~~~~~~ - -Provides support for verisure components +~~~~~~~~~~~~~~~~~~~ +Provides support for verisure components. Configuration: +To use the Verisure component you will need to add something like the +following to your configuration.yaml file. + verisure: username: user@example.com password: password @@ -14,32 +16,31 @@ verisure: smartplugs: 1 thermometers: 0 - Variables: username *Required -Username to verisure mypages +Username to Verisure mypages. password *Required -Password to verisure mypages +Password to Verisure mypages. alarm -*Opional -Set to 1 to show alarm, 0 to disable. Default 1 +*Optional +Set to 1 to show alarm, 0 to disable. Default 1. hygrometers -*Opional -Set to 1 to show hygrometers, 0 to disable. Default 1 +*Optional +Set to 1 to show hygrometers, 0 to disable. Default 1. smartplugs -*Opional -Set to 1 to show smartplugs, 0 to disable. Default 1 +*Optional +Set to 1 to show smartplugs, 0 to disable. Default 1. thermometers -*Opional -Set to 1 to show thermometers, 0 to disable. Default 1 +*Optional +Set to 1 to show thermometers, 0 to disable. Default 1. """ import logging from datetime import timedelta @@ -62,8 +63,9 @@ DISCOVER_ALARMS = 'verisure.alarms_control_panel' DEPENDENCIES = ['alarm_control_panel'] REQUIREMENTS = [ - 'https://github.com/persandstrom/python-verisure/archive/master.zip' - ] + 'https://github.com/persandstrom/python-verisure/archive/' + '9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6' +] _LOGGER = logging.getLogger(__name__) @@ -136,22 +138,22 @@ def setup(hass, config): def get_alarm_status(): - ''' return a list of status overviews for alarm components ''' + """ Return a list of status overviews for alarm components. """ return STATUS[MY_PAGES.DEVICE_ALARM] def get_climate_status(): - ''' return a list of status overviews for alarm components ''' + """ Return a list of status overviews for alarm components. """ return STATUS[MY_PAGES.DEVICE_CLIMATE] def get_smartplug_status(): - ''' return a list of status overviews for alarm components ''' + """ Return a list of status overviews for alarm components. """ return STATUS[MY_PAGES.DEVICE_SMARTPLUG] def reconnect(): - ''' reconnect to verisure mypages ''' + """ Reconnect to verisure mypages. """ try: MY_PAGES.login() except VERISURE_LOGIN_ERROR as ex: @@ -164,7 +166,7 @@ def reconnect(): @Throttle(MIN_TIME_BETWEEN_REQUESTS) def update(): - ''' Updates the status of verisure components ''' + """ Updates the status of verisure components. """ if WRONG_PASSWORD_GIVEN: _LOGGER.error('Wrong password') return diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index d56a244b84c..c05d9502ca7 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -1,8 +1,22 @@ """ homeassistant.components.wink ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Connects to a Wink hub and loads relevant components to control its devices. + +Configuration: + +To use the Wink component you will need to add something like the following +to your configuration.yaml file. + +wink: + access_token: YOUR_ACCESS_TOKEN + +Variables: + +access_token +*Required +Please check https://home-assistant.io/components/wink.html for further +details. """ import logging @@ -16,8 +30,9 @@ 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' + '#python-wink==0.1'] 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 54ad297e62a..2ec706e4512 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) @@ -39,6 +40,13 @@ DEFAULT_COMPONENTS = { } +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): """ Ensures a config file exists in given config dir. Creating a default one if needed. @@ -46,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 in", + config_dir) config_path = create_default_config(config_dir, detect_location) return config_path @@ -92,9 +101,7 @@ def create_default_config(config_dir, detect_location=True): 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 @@ -117,7 +124,7 @@ def load_yaml_config_file(config_path): def parse(fname): """ Parse a YAML file. """ try: - with open(fname) as conf_file: + with open(fname, encoding='utf-8') as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict return yaml.load(conf_file) or {} diff --git a/homeassistant/const.py b/homeassistant/const.py index 86c5a7ab6a5..21a2e6c41e3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,7 @@ """ Constants used by Home Assistant components. """ + +__version__ = "0.7.3dev0" + # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' @@ -37,12 +40,13 @@ STATE_ON = 'on' STATE_OFF = 'off' STATE_HOME = 'home' STATE_NOT_HOME = 'not_home' -STATE_UNKNOWN = "unknown" +STATE_UNKNOWN = 'unknown' STATE_OPEN = 'open' STATE_CLOSED = 'closed' STATE_PLAYING = 'playing' STATE_PAUSED = 'paused' STATE_IDLE = 'idle' +STATE_STANDBY = 'standby' STATE_ALARM_DISARMED = 'disarmed' STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' diff --git a/homeassistant/core.py b/homeassistant/core.py index 76b4b38f3fc..df18d7e7902 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -9,6 +9,7 @@ of entities and react to changes. import os import time import logging +import signal import threading import enum import re @@ -21,9 +22,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" @@ -70,13 +74,20 @@ class HomeAssistant(object): will block until called. """ request_shutdown = threading.Event() - def stop_homeassistant(service): + def stop_homeassistant(*args): """ Stops Home Assistant. """ request_shutdown.set() self.services.register( DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) + if os.name != "nt": + try: + signal.signal(signal.SIGQUIT, stop_homeassistant) + except ValueError: + _LOGGER.warning( + 'Could not bind to SIGQUIT. Are you running in a thread?') + while not request_shutdown.isSet(): try: time.sleep(1) @@ -653,6 +664,9 @@ class Config(object): self.location_name = None self.time_zone = None + # If True, pip install is skipped for requirements on startup + self.skip_pip = False + # List of loaded components self.components = [] @@ -660,7 +674,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 +709,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). @@ -773,8 +772,10 @@ def create_timer(hass, interval=TIMER_INTERVAL): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_timer) -def create_worker_pool(worker_count=MIN_WORKER_THREAD): +def create_worker_pool(worker_count=None): """ Creates a worker pool to be used. """ + if worker_count is None: + worker_count = MIN_WORKER_THREAD def job_handler(job): """ Called whenever a job is available to do. """ 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/external/noop b/homeassistant/external/noop deleted file mode 160000 index 4eaeb3367f9..00000000000 --- a/homeassistant/external/noop +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4eaeb3367f9ada05dae3319cf24ab1da5de1aa89 diff --git a/homeassistant/external/nzbclients b/homeassistant/external/nzbclients deleted file mode 160000 index f01997498fe..00000000000 --- a/homeassistant/external/nzbclients +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f01997498fe190d6ac2a2c375a739024843bd44d diff --git a/homeassistant/external/vera b/homeassistant/external/vera deleted file mode 160000 index 30c59781d63..00000000000 --- a/homeassistant/external/vera +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 30c59781d63322db2160ff00a4b99f16ead40b85 diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 0ca63856c27..82dafac5576 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -7,11 +7,11 @@ 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, - STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, TEMP_CELCIUS, + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_DEFAULT_NAME, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELCIUS, TEMP_FAHRENHEIT) # Dict mapping entity_id to a boolean that overwrites the hidden property @@ -44,17 +44,17 @@ class Entity(object): @property def name(self): """ Returns the name of the entity. """ - return self.get_name() + return DEVICE_DEFAULT_NAME @property def state(self): """ Returns the state of the entity. """ - return self.get_state() + return STATE_UNKNOWN @property def state_attributes(self): """ Returns the state attributes. """ - return {} + return None @property def unit_of_measurement(self): @@ -64,34 +64,12 @@ class Entity(object): @property def hidden(self): """ Suggestion if the entity should be hidden from UIs. """ - return self._hidden - - @hidden.setter - def hidden(self, val): - """ Sets the suggestion for visibility. """ - self._hidden = bool(val) + return False def update(self): """ Retrieve latest state. """ pass - # DEPRECATION NOTICE: - # Device is moving from getters to properties. - # For now the new properties will call the old functions - # This will be removed in the future. - - def get_name(self): - """ Returns the name of the entity if any. """ - return DEVICE_DEFAULT_NAME - - def get_state(self): - """ Returns state of the entity. """ - return "Unknown" - - def get_state_attributes(self): - """ Returns optional state attributes. """ - return None - # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 80084178fe0..708bf6e93a9 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -129,13 +129,13 @@ class EntityComponent(object): if platform is None: return - platform_name = '{}.{}'.format(self.domain, platform_type) - try: platform.setup_platform( self.hass, platform_config, self.add_entities, discovery_info) - - self.hass.config.components.append(platform_name) except Exception: # pylint: disable=broad-except self.logger.exception( 'Error while setting up platform %s', platform_type) + return + + platform_name = '{}.{}'.format(self.domain, platform_type) + self.hass.config.components.append(platform_name) 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/__init__.py b/homeassistant/util/__init__.py index 2e399384e63..5c77fa37814 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -71,7 +71,7 @@ def ensure_unique_string(preferred_string, current_strings): """ Returns a string that is not present in current_strings. If preferred string exists will append _2, _3, .. """ test_string = preferred_string - current_strings = list(current_strings) + current_strings = set(current_strings) tries = 1 @@ -244,22 +244,22 @@ class Throttle(object): Wrapper that allows wrapped to be called only once per min_time. If we cannot acquire the lock, it is running so return None. """ - if lock.acquire(False): - try: - last_call = wrapper.last_call + if not lock.acquire(False): + return None + try: + last_call = wrapper.last_call - # Check if method is never called or no_throttle is given - force = not last_call or kwargs.pop('no_throttle', False) + # Check if method is never called or no_throttle is given + force = not last_call or kwargs.pop('no_throttle', False) - if force or datetime.now() - last_call > self.min_time: - - result = method(*args, **kwargs) - wrapper.last_call = datetime.now() - return result - else: - return None - finally: - lock.release() + if force or utcnow() - last_call > self.min_time: + result = method(*args, **kwargs) + wrapper.last_call = utcnow() + return result + else: + return None + finally: + lock.release() wrapper.last_call = None diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index d220a5a7e61..5d32c087efe 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,23 +1,58 @@ """Helpers to install PyPi packages.""" +import os +import logging +import pkg_resources import subprocess import sys +import threading +from urllib.parse import urlparse -from . import environment as env - -# If we are not in a virtual environment, install in user space -INSTALL_USER = not env.is_virtual() +_LOGGER = logging.getLogger(__name__) +INSTALL_LOCK = threading.Lock() -def install_package(package, upgrade=False, user=INSTALL_USER): +def install_package(package, upgrade=True, 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)] + + with INSTALL_LOCK: + if check_package_exists(package, target): + return True + + _LOGGER.info('Attempting install of %s', package) + try: + return 0 == subprocess.call(args) + except subprocess.SubprocessError: + return False + + +def check_package_exists(package, target=None): + """Check if a package exists. + Returns True when the requirement is met. + Returns False when the package is not installed or doesn't meet req.""" try: - return 0 == subprocess.call(args) - except subprocess.SubprocessError: + req = pkg_resources.Requirement.parse(package) + except ValueError: + # This is a zip file + req = pkg_resources.Requirement.parse(urlparse(package).fragment) + + if target: + work_set = pkg_resources.WorkingSet([target]) + search_fun = work_set.find + + else: + search_fun = pkg_resources.get_distribution + + try: + result = search_fun(req) + except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): return False + + return bool(result) diff --git a/pylintrc b/pylintrc index 54b1f80cdc5..e8455cf4245 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,4 @@ [MASTER] -ignore=external reports=no # Reasons disabled: diff --git a/requirements.txt b/requirements.txt index d27815e13cd..1b7d2396971 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,115 +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.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/master.zip - -# Mysensors -https://github.com/theolind/pymysensors/archive/master.zip#egg=pymysensors-0.1 - -# 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/master.zip#pywink>=0.1 - -# Slack notifier (notify.slack) -slacker>=0.6.8 - -# Temper sensors (sensor.temper) -https://github.com/rkabadi/temper-python/archive/master.zip - -# PyEdimax -https://github.com/rkabadi/pyedimax/archive/master.zip - -# RPI-GPIO platform (*.rpi_gpio) -RPi.GPIO >=0.5.11 - -# PAHO MQTT Binding (mqtt) -paho-mqtt>=1.1 - -# PyModbus (modbus) -https://github.com/bashwork/pymodbus/archive/python3.zip#pymodbus>=1.2.0 - -# Verisure (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..6a0ae0e0169 --- /dev/null +++ b/requirements_all.txt @@ -0,0 +1,135 @@ +# 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.12 + +# Keyboard (keyboard) +pyuserinput==0.1.9 + +# Tellstick bindings (*.tellstick) +tellcore-py==1.0.4 + +# Nmap bindings (device_tracker.nmap) +python-nmap==0.4.1 + +# PushBullet bindings (notify.pushbullet) +pushbullet.py==0.7.1 + +# Nest Thermostat bindings (thermostat.nest) +python-nest==2.6.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#RFXtrx==0.15 + +# Mysensors +https://github.com/theolind/pymysensors/archive/35b87d880147a34107da0d40cb815d75e6cb4af7.zip#pymysensors==0.2 + +# Netgear (device_tracker.netgear) +pynetgear==0.3 + +# Netdisco (discovery) +netdisco==0.4 + +# Wemo (switch.wemo) +pywemo==0.3 + +# Wink (*.wink) +https://github.com/balloob/python-wink/archive/c2b700e8ca866159566ecf5e644d9c297f69f257.zip#python-wink==0.1 + +# Slack notifier (notify.slack) +slacker==0.6.8 + +# Temper sensors (sensor.temper) +https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3 + +# PyEdimax +https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 + +# 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#Adafruit_DHT==1.1.0 + +# PAHO MQTT Binding (mqtt) +paho-mqtt==1.1 + +# PyModbus (modbus) +https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 + +# Verisure (verisure) +https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 + +# Python tools for interacting with IFTTT Maker Channel (ifttt) +pyfttt==0.3 + +# sensor.sabnzbd +https://github.com/balloob/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 + +# switch.vera +# sensor.vera +# light.vera +https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip#python-vera==0.1 + +# Sonos bindings (media_player.sonos) +SoCo==0.11.1 diff --git a/scripts/check_style b/scripts/check_style index cacebba15a1..5fc8861b91a 100755 --- a/scripts/check_style +++ b/scripts/check_style @@ -5,5 +5,5 @@ if [ ${PWD##*/} == "scripts" ]; then cd .. fi +flake8 homeassistant pylint homeassistant -flake8 homeassistant --exclude bower_components,external diff --git a/scripts/hass-daemon b/scripts/hass-daemon new file mode 100644 index 00000000000..d11c2669e87 --- /dev/null +++ b/scripts/hass-daemon @@ -0,0 +1,101 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: hass +# Required-Start: $local_fs $network $named $time $syslog +# Required-Stop: $local_fs $network $named $time $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: Home\ Assistant +### END INIT INFO + +# /etc/init.d Service Script for Home Assistant +# Created with: https://gist.github.com/naholyr/4275302#file-new-service-sh +# +# Installation: +# 1) If any commands need to run before executing hass (like loading a +# virutal environment), put them in PRE_EXEC. This command must end with +# a semicolon. +# 2) Set RUN_AS to the username that should be used to execute hass. +# 3) Copy this script to /etc/init.d/ +# sudo cp hass-daemon /etc/init.d/hass-daemon +# sudo chmod +x /etc/init.d/hass-daemon +# 4) Register the daemon with Linux +# sudo update-rc.d hass-daemon defaults +# 5) Install this service +# sudo service hass-daemon install +# 6) Restart Machine +# +# After installation, HA should start automatically. If HA does not start, +# check the log file output for errors. +# /var/opt/homeassistant/home-assistant.log + +PRE_EXEC="" +RUN_AS="USER" +PID_FILE="/var/run/hass.pid" +CONFIG_DIR="/var/opt/homeassistant" +FLAGS="-v --config $CONFIG_DIR --pid-file $PID_FILE --daemon" + +start() { + if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE); then + echo 'Service already running' >&2 + return 1 + fi + echo 'Starting service…' >&2 + local CMD="$PRE_EXEC hass $FLAGS;" + su -c "$CMD" $RUN_AS + echo 'Service started' >&2 +} + +stop() { + if [ ! -f "$PID_FILE" ] || ! kill -0 $(cat "$PID_FILE"); then + echo 'Service not running' >&2 + return 1 + fi + echo 'Stopping service…' >&2 + kill -3 $(cat "$PID_FILE") + echo 'Service stopped' >&2 +} + +install() { + echo "Installing Home Assistant Daemon (hass-daemon)" + echo "999999" > $PID_FILE + chown $RUN_AS $PID_FILE + mkdir -p $CONFIG_DIR + chown $RUN_AS $CONFIG_DIR +} + +uninstall() { + echo -n "Are you really sure you want to uninstall this service? That cannot be undone. [yes|No] " + local SURE + read SURE + if [ "$SURE" = "yes" ]; then + stop + rm -fv "$PID_FILE" + echo "Notice: The config directory has not been removed" + echo $CONFIG_DIR + update-rc.d -f hass-daemon remove + rm -fv "$0" + echo "Home Assistant Daemon has been removed. Home Assistant is still installed." + fi +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + install) + install + ;; + uninstall) + uninstall + ;; + restart) + stop + start + ;; + *) + echo "Usage: $0 {start|stop|restart|install|uninstall}" +esac diff --git a/scripts/homeassistant-pi.sh b/scripts/homeassistant-pi.sh deleted file mode 100755 index 1d5537f0191..00000000000 --- a/scripts/homeassistant-pi.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/sh - -# To script is for running Home Assistant as a service and automatically starting it on boot. -# Assuming you have cloned the HA repo into /home/pi/Apps/home-assistant adjust this path if necessary -# This also assumes you installed HA on your raspberry pi using the instructions here: -# https://home-assistant.io/getting-started/ -# -# To install to the following: -# sudo cp /home/pi/Apps/home-assistant/scripts/homeassistant-pi.sh /etc/init.d/homeassistant.sh -# sudo chmod +x /etc/init.d/homeassistant.sh -# sudo chown root:root /etc/init.d/homeassistant.sh -# -# If you want HA to start on boot also run the following: -# sudo update-rc.d homeassistant.sh defaults -# sudo update-rc.d homeassistant.sh enable -# -# You should now be able to start HA by running -# sudo /etc/init.d/homeassistant.sh start - -### BEGIN INIT INFO -# Provides: myservice -# Required-Start: $remote_fs $syslog -# Required-Stop: $remote_fs $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Put a short description of the service here -# Description: Put a long description of the service here -### END INIT INFO - -# Change the next 3 lines to suit where you install your script and what you want to call it -DIR=/home/pi/Apps/home-assistant -DAEMON="/home/pi/.pyenv/shims/python3 -m homeassistant" -DAEMON_NAME=homeassistant - -# Add any command line options for your daemon here -DAEMON_OPTS="" - -# This next line determines what user the script runs as. -# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. -DAEMON_USER=pi - -# The process ID of the script when it runs is stored here: -PIDFILE=/var/run/$DAEMON_NAME.pid - -. /lib/lsb/init-functions - -do_start () { - log_daemon_msg "Starting system $DAEMON_NAME daemon" - start-stop-daemon --start --background --chdir $DIR --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS - log_end_msg $? -} -do_stop () { - log_daemon_msg "Stopping system $DAEMON_NAME daemon" - start-stop-daemon --stop --pidfile $PIDFILE --retry 10 - log_end_msg $? -} - -case "$1" in - - start|stop) - do_${1} - ;; - - restart|reload|force-reload) - do_stop - do_start - ;; - - status) - status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? - ;; - *) - echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" - exit 1 - ;; - -esac -exit 0 diff --git a/scripts/homeassistant.daemon b/scripts/homeassistant.daemon deleted file mode 100755 index bef4cd90f4a..00000000000 --- a/scripts/homeassistant.daemon +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/sh -### BEGIN INIT INFO -# Provides: homeassistant -# Required-Start: $local_fs $network $named $time $syslog -# Required-Stop: $local_fs $network $named $time $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Description: Home\ Assistant -### END INIT INFO - -# /etc/init.d Service Script for Home Assistant -# Created with: https://gist.github.com/naholyr/4275302#file-new-service-sh -# -# Installation: -# 1) Populate RUNAS and RUNDIR variables -# 2) Create Log -- sudo touch /var/log/homeassistant.log -# 3) Set Log Ownership -- sudo chown USER:GROUP /var/log/homeassistant.log -# 4) Create PID File -- sudo touch /var/run/homeassistant.pid -# 5) Set PID File Ownership -- sudo chown USER:GROUP /var/run/homeassistant.pid -# 6) Install init.d script -- cp homeassistant.daemon /etc/init.d/homeassistant -# 7) Setup HA for Auto Start -- sudo update-rc.d homeassistant defaults -# 8) Run HA -- sudo service homeassistant start -# -# After installation, HA should start automatically. If HA does not start, -# check the log file output for errors. (/var/log/homeassistant.log) -# -# For this script, it is assumed that you are using a local Virtual Environment -# per the install directions as http://home-assistant.io -# If you are not, the SCRIPT variable must be modified to point to the correct -# Python environment. - -SCRIPT="source bin/activate; ./bin/python -m homeassistant" -RUNAS= -RUNDIR= -PIDFILE=/var/run/homeassistant.pid -LOGFILE=/var/log/homeassistant.log - -start() { - if [ -f /var/run/$PIDNAME ] && kill -0 $(cat /var/run/$PIDNAME); then - echo 'Service already running' >&2 - return 1 - fi - echo 'Starting service…' >&2 - local CMD="cd $RUNDIR; $SCRIPT &> \"$LOGFILE\" & echo \$!" - su -c "$CMD" $RUNAS > "$PIDFILE" - echo 'Service started' >&2 -} - -stop() { - if [ ! -f "$PIDFILE" ] || ! kill -0 $(cat "$PIDFILE"); then - echo 'Service not running' >&2 - return 1 - fi - echo 'Stopping service…' >&2 - kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE" - echo 'Service stopped' >&2 -} - -uninstall() { - echo -n "Are you really sure you want to uninstall this service? That cannot be undone. [yes|No] " - local SURE - read SURE - if [ "$SURE" = "yes" ]; then - stop - rm -f "$PIDFILE" - echo "Notice: log file is not be removed: '$LOGFILE'" >&2 - update-rc.d -f homeassistant remove - rm -fv "$0" - fi -} - -case "$1" in - start) - start - ;; - stop) - stop - ;; - uninstall) - uninstall - ;; - retart) - stop - start - ;; - *) - echo "Usage: $0 {start|stop|restart|uninstall}" -esac diff --git a/scripts/update b/scripts/update index afeacbb1235..be5e8fc01bf 100755 --- a/scripts/update +++ b/scripts/update @@ -1,6 +1,6 @@ -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi - -git pull +echo "The update script has been deprecated since Home Assistant v0.7" +echo +echo "Home Assistant is now distributed via PyPi and can be installed and" +echo "upgraded by running: pip3 install --upgrade homeassistant" +echo +echo "If you are developing a new feature for Home Assistant, run: git pull" diff --git a/setup.py b/setup.py new file mode 100755 index 00000000000..ce8a75072ac --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +import os +from setuptools import setup, find_packages +from homeassistant.const import __version__ + +PACKAGE_NAME = 'homeassistant' +HERE = os.path.abspath(os.path.dirname(__file__)) +DOWNLOAD_URL = ('https://github.com/balloob/home-assistant/archive/' + '{}.zip'.format(__version__)) + +PACKAGES = find_packages(exclude=['tests', 'tests.*']) + +PACKAGE_DATA = \ + {'homeassistant.components.frontend': ['index.html.template'], + 'homeassistant.components.frontend.www_static': ['*.*'], + 'homeassistant.components.frontend.www_static.images': ['*.*']} + +REQUIRES = [ + 'requests>=2,<3', + 'pyyaml>=3.11,<4', + 'pytz>=2015.4', + 'pip>=7.0.0', +] + +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/__init__.py b/tests/__init__.py new file mode 100644 index 00000000000..c39a22e0b57 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +import logging +logging.disable(logging.CRITICAL) diff --git a/tests/common.py b/tests/common.py index be6aa623a25..830b21ed47c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -8,13 +8,13 @@ import os from datetime import timedelta from unittest import mock -import homeassistant.core as ha +from homeassistant import core as ha, loader import homeassistant.util.location as location_util -import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, - EVENT_STATE_CHANGED) + EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, + ATTR_DISCOVERED) from homeassistant.components import sun, mqtt @@ -38,6 +38,9 @@ def get_test_home_assistant(num_threads=None): hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 + if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: + loader.prepare(hass) + return hass @@ -83,10 +86,11 @@ def fire_time_changed(hass, time): hass.bus.fire(EVENT_TIME_CHANGED, {'now': time}) -def trigger_device_tracker_scan(hass): - """ Triggers the device tracker to scan. """ - fire_time_changed( - hass, dt_util.utcnow().replace(second=0) + timedelta(hours=1)) +def fire_service_discovered(hass, service, info): + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: service, + ATTR_DISCOVERED: info + }) def ensure_sun_risen(hass): diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 0f11a2a67c5..05c5ade1d53 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -21,7 +21,6 @@ class TestAutomationTime(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name self.hass = ha.HomeAssistant() - loader.prepare(self.hass) self.calls = [] def record_call(service): diff --git a/tests/components/device_tracker/__init__.py b/tests/components/device_tracker/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py new file mode 100644 index 00000000000..40f8a12c2d0 --- /dev/null +++ b/tests/components/device_tracker/test_init.py @@ -0,0 +1,235 @@ +""" +tests.test_component_device_tracker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the device tracker compoments. +""" +# pylint: disable=protected-access,too-many-public-methods +import unittest +from unittest.mock import patch +from datetime import timedelta +import os + +from homeassistant.config import load_yaml_config_file +from homeassistant.loader import get_component +import homeassistant.util.dt as dt_util +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, + STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM, DEVICE_DEFAULT_NAME) +import homeassistant.components.device_tracker as device_tracker + +from tests.common import ( + get_test_home_assistant, fire_time_changed, fire_service_discovered) + + +class TestComponentsDeviceTracker(unittest.TestCase): + """ Tests homeassistant.components.device_tracker module. """ + + def setUp(self): # pylint: disable=invalid-name + """ Init needed objects. """ + self.hass = get_test_home_assistant() + self.yaml_devices = self.hass.config.path(device_tracker.YAML_DEVICES) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + try: + os.remove(self.yaml_devices) + except FileNotFoundError: + pass + + self.hass.stop() + + def test_is_on(self): + """ Test is_on method. """ + entity_id = device_tracker.ENTITY_ID_FORMAT.format('test') + + self.hass.states.set(entity_id, STATE_HOME) + + self.assertTrue(device_tracker.is_on(self.hass, entity_id)) + + self.hass.states.set(entity_id, STATE_NOT_HOME) + + self.assertFalse(device_tracker.is_on(self.hass, entity_id)) + + def test_migrating_config(self): + csv_devices = self.hass.config.path(device_tracker.CSV_DEVICES) + + self.assertFalse(os.path.isfile(csv_devices)) + self.assertFalse(os.path.isfile(self.yaml_devices)) + + person1 = { + 'mac': 'AB:CD:EF:GH:IJ:KL', + 'name': 'Paulus', + 'track': True, + 'picture': 'http://placehold.it/200x200', + } + person2 = { + 'mac': 'MN:OP:QR:ST:UV:WX:YZ', + 'name': '', + 'track': False, + 'picture': None, + } + + try: + with open(csv_devices, 'w') as fil: + fil.write('device,name,track,picture\n') + for pers in (person1, person2): + fil.write('{},{},{},{}\n'.format( + pers['mac'], pers['name'], + '1' if pers['track'] else '0', pers['picture'] or '')) + + self.assertTrue(device_tracker.setup(self.hass, {})) + self.assertFalse(os.path.isfile(csv_devices)) + self.assertTrue(os.path.isfile(self.yaml_devices)) + + yaml_config = load_yaml_config_file(self.yaml_devices) + + self.assertEqual(2, len(yaml_config)) + + for pers, yaml_pers in zip( + (person1, person2), sorted(yaml_config.values(), + key=lambda pers: pers['mac'])): + for key, value in pers.items(): + if key == 'name' and value == '': + value = DEVICE_DEFAULT_NAME + self.assertEqual(value, yaml_pers.get(key)) + + finally: + try: + os.remove(csv_devices) + except FileNotFoundError: + pass + + def test_reading_yaml_config(self): + dev_id = 'test' + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, dev_id, 'AB:CD:EF:GH:IJ', + 'Test name', 'http://test.picture', True) + device_tracker.update_config(self.yaml_devices, dev_id, device) + self.assertTrue(device_tracker.setup(self.hass, {})) + config = device_tracker.load_config(self.yaml_devices, self.hass, + device.consider_home)[0] + self.assertEqual(device.dev_id, config.dev_id) + self.assertEqual(device.track, config.track) + self.assertEqual(device.mac, config.mac) + self.assertEqual(device.config_picture, config.config_picture) + self.assertEqual(device.away_hide, config.away_hide) + self.assertEqual(device.consider_home, config.consider_home) + + def test_setup_without_yaml_file(self): + self.assertTrue(device_tracker.setup(self.hass, {})) + + def test_adding_unknown_device_to_config(self): + scanner = get_component('device_tracker.test').SCANNER + scanner.reset() + scanner.come_home('DEV1') + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + config = device_tracker.load_config(self.yaml_devices, self.hass, + timedelta(seconds=0))[0] + self.assertEqual('DEV1', config.dev_id) + self.assertEqual(True, config.track) + + def test_discovery(self): + scanner = get_component('device_tracker.test').SCANNER + + with patch.dict(device_tracker.DISCOVERY_PLATFORMS, {'test': 'test'}): + with patch.object(scanner, 'scan_devices') as mock_scan: + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + fire_service_discovered(self.hass, 'test', {}) + self.assertTrue(mock_scan.called) + + def test_update_stale(self): + scanner = get_component('device_tracker.test').SCANNER + scanner.reset() + scanner.come_home('DEV1') + + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + self.assertEqual(STATE_HOME, + self.hass.states.get('device_tracker.dev1').state) + + scanner.leave_home('DEV1') + + now = dt_util.utcnow().replace(second=0) + timedelta(hours=1) + + with patch('homeassistant.util.dt.utcnow', return_value=now): + fire_time_changed(self.hass, now) + self.hass.pool.block_till_done() + + self.assertEqual(STATE_NOT_HOME, + self.hass.states.get('device_tracker.dev1').state) + + def test_entity_attributes(self): + dev_id = 'test_entity' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + friendly_name = 'Paulus' + picture = 'http://placehold.it/200x200' + + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, dev_id, None, + friendly_name, picture, away_hide=True) + device_tracker.update_config(self.yaml_devices, dev_id, device) + + self.assertTrue(device_tracker.setup(self.hass, {})) + + attrs = self.hass.states.get(entity_id).attributes + + self.assertEqual(friendly_name, attrs.get(ATTR_FRIENDLY_NAME)) + self.assertEqual(picture, attrs.get(ATTR_ENTITY_PICTURE)) + + def test_device_hidden(self): + dev_id = 'test_entity' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, dev_id, None, + away_hide=True) + device_tracker.update_config(self.yaml_devices, dev_id, device) + + scanner = get_component('device_tracker.test').SCANNER + scanner.reset() + + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + + self.assertTrue(self.hass.states.get(entity_id) + .attributes.get(ATTR_HIDDEN)) + + def test_group_all_devices(self): + dev_id = 'test_entity' + entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + device = device_tracker.Device( + self.hass, timedelta(seconds=180), True, dev_id, None, + away_hide=True) + device_tracker.update_config(self.yaml_devices, dev_id, device) + + scanner = get_component('device_tracker.test').SCANNER + scanner.reset() + + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) + + state = self.hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES) + self.assertIsNotNone(state) + self.assertEqual(STATE_NOT_HOME, state.state) + self.assertSequenceEqual((entity_id,), + state.attributes.get(ATTR_ENTITY_ID)) + + @patch('homeassistant.components.device_tracker.DeviceTracker.see') + def test_see_service(self, mock_see): + self.assertTrue(device_tracker.setup(self.hass, {})) + mac = 'AB:CD:EF:GH' + dev_id = 'some_device' + host_name = 'example.com' + location_name = 'Work' + gps = [.3, .8] + + device_tracker.see(self.hass, mac, dev_id, host_name, location_name, + gps) + + self.hass.pool.block_till_done() + + mock_see.assert_called_once_with( + mac=mac, dev_id=dev_id, host_name=host_name, + location_name=location_name, gps=gps) diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py new file mode 100644 index 00000000000..6e219621a60 --- /dev/null +++ b/tests/components/device_tracker/test_mqtt.py @@ -0,0 +1,37 @@ +import unittest +import os + +from homeassistant.components import device_tracker +from homeassistant.const import CONF_PLATFORM + +from tests.common import ( + get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) + + +class TestComponentsDeviceTrackerMQTT(unittest.TestCase): + def setUp(self): # pylint: disable=invalid-name + """ Init needed objects. """ + self.hass = get_test_home_assistant() + mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + try: + os.remove(self.hass.config.path(device_tracker.YAML_DEVICES)) + except FileNotFoundError: + pass + + def test_new_message(self): + dev_id = 'paulus' + enttiy_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) + topic = '/location/paulus' + location = 'work' + + self.assertTrue(device_tracker.setup(self.hass, { + device_tracker.DOMAIN: { + CONF_PLATFORM: 'mqtt', + 'devices': {dev_id: topic} + }})) + fire_mqtt_message(self.hass, topic, location) + self.hass.pool.block_till_done() + self.assertEqual(location, self.hass.states.get(enttiy_id).state) diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 93b1cd06abe..b267e6b3c1c 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -7,6 +7,7 @@ Tests Home Assistant HTTP component does what it should do. # pylint: disable=protected-access,too-many-public-methods import unittest import json +from unittest.mock import patch import requests @@ -35,7 +36,9 @@ def _url(path=""): return HTTP_BASE_URL + path -def setUpModule(): # pylint: disable=invalid-name +@patch('homeassistant.components.http.util.get_local_ip', + return_value='127.0.0.1') +def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name """ Initalizes a Home Assistant server. """ global hass diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py new file mode 100644 index 00000000000..243fe128b28 --- /dev/null +++ b/tests/components/test_conversation.py @@ -0,0 +1,111 @@ +""" +tests.components.test_conversation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests Conversation component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest +from unittest.mock import patch + +import homeassistant.components as core_components +from homeassistant.components import conversation +from homeassistant.const import ATTR_ENTITY_ID + +from tests.common import get_test_home_assistant + + +class TestConversation(unittest.TestCase): + """ Test the conversation component. """ + + def setUp(self): # pylint: disable=invalid-name + """ Start up ha for testing """ + self.ent_id = 'light.kitchen_lights' + self.hass = get_test_home_assistant(3) + self.hass.states.set(self.ent_id, 'on') + self.assertTrue(core_components.setup(self.hass, {})) + self.assertTrue( + conversation.setup(self.hass, {conversation.DOMAIN: {}})) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_turn_on(self): + """ Setup and perform good turn on requests """ + calls = [] + + def record_call(service): + calls.append(service) + + self.hass.services.register('light', 'turn_on', record_call) + + event_data = {conversation.ATTR_TEXT: 'turn kitchen lights on'} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + + call = calls[-1] + self.assertEqual('light', call.domain) + self.assertEqual('turn_on', call.service) + self.assertEqual([self.ent_id], call.data[ATTR_ENTITY_ID]) + + def test_turn_off(self): + """ Setup and perform good turn off requests """ + calls = [] + + def record_call(service): + calls.append(service) + + self.hass.services.register('light', 'turn_off', record_call) + + event_data = {conversation.ATTR_TEXT: 'turn kitchen lights off'} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + + call = calls[-1] + self.assertEqual('light', call.domain) + self.assertEqual('turn_off', call.service) + self.assertEqual([self.ent_id], call.data[ATTR_ENTITY_ID]) + + @patch('homeassistant.components.conversation.logging.Logger.error') + @patch('homeassistant.core.ServiceRegistry.call') + def test_bad_request_format(self, mock_logger, mock_call): + """ Setup and perform a badly formatted request """ + event_data = { + conversation.ATTR_TEXT: + 'what is the answer to the ultimate question of life, ' + + 'the universe and everything'} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + self.assertTrue(mock_logger.called) + self.assertFalse(mock_call.called) + + @patch('homeassistant.components.conversation.logging.Logger.error') + @patch('homeassistant.core.ServiceRegistry.call') + def test_bad_request_entity(self, mock_logger, mock_call): + """ Setup and perform requests with bad entity id """ + event_data = {conversation.ATTR_TEXT: 'turn something off'} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + self.assertTrue(mock_logger.called) + self.assertFalse(mock_call.called) + + @patch('homeassistant.components.conversation.logging.Logger.error') + @patch('homeassistant.core.ServiceRegistry.call') + def test_bad_request_command(self, mock_logger, mock_call): + """ Setup and perform requests with bad command """ + event_data = {conversation.ATTR_TEXT: 'turn kitchen lights over'} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + self.assertTrue(mock_logger.called) + self.assertFalse(mock_call.called) + + @patch('homeassistant.components.conversation.logging.Logger.error') + @patch('homeassistant.core.ServiceRegistry.call') + def test_bad_request_notext(self, mock_logger, mock_call): + """ Setup and perform requests with bad command with no text """ + event_data = {} + self.assertTrue(self.hass.services.call( + conversation.DOMAIN, 'process', event_data, True)) + self.assertTrue(mock_logger.called) + self.assertFalse(mock_call.called) diff --git a/tests/components/test_demo.py b/tests/components/test_demo.py index 0abd546e4c4..13cc55ed7dc 100644 --- a/tests/components/test_demo.py +++ b/tests/components/test_demo.py @@ -4,14 +4,18 @@ tests.test_component_demo Tests demo component. """ +import json import unittest +from unittest.mock import patch import homeassistant.core as ha import homeassistant.components.demo as demo +from homeassistant.remote import JSONEncoder from tests.common import mock_http_component +@patch('homeassistant.components.sun.setup') class TestDemo(unittest.TestCase): """ Test the demo module. """ @@ -23,14 +27,24 @@ class TestDemo(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_if_demo_state_shows_by_default(self): + def test_if_demo_state_shows_by_default(self, mock_sun_setup): """ Test if demo state shows if we give no configuration. """ demo.setup(self.hass, {demo.DOMAIN: {}}) self.assertIsNotNone(self.hass.states.get('a.Demo_Mode')) - def test_hiding_demo_state(self): + def test_hiding_demo_state(self, mock_sun_setup): """ Test if you can hide the demo card. """ demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': 1}}) self.assertIsNone(self.hass.states.get('a.Demo_Mode')) + + def test_all_entities_can_be_loaded_over_json(self, mock_sun_setup): + """ Test if you can hide the demo card. """ + demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': 1}}) + + try: + json.dumps(self.hass.states.all(), cls=JSONEncoder) + except Exception: + self.fail('Unable to convert all demo entities to JSON. ' + 'Wrong data in state machine!') diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 05b2bf11bea..f3ec23a96bf 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -9,14 +9,14 @@ import os import unittest import homeassistant.loader as loader -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.components import ( device_tracker, light, sun, device_sun_light_trigger) from tests.common import ( - get_test_home_assistant, ensure_sun_risen, ensure_sun_set, - trigger_device_tracker_scan) + get_test_config_dir, get_test_home_assistant, ensure_sun_risen, + ensure_sun_set) KNOWN_DEV_PATH = None @@ -26,13 +26,8 @@ def setUpModule(): # pylint: disable=invalid-name """ Initalizes a Home Assistant server. """ global KNOWN_DEV_PATH - hass = get_test_home_assistant() - - loader.prepare(hass) - KNOWN_DEV_PATH = hass.config.path( - device_tracker.KNOWN_DEVICES_FILE) - - hass.stop() + KNOWN_DEV_PATH = os.path.join(get_test_config_dir(), + device_tracker.CSV_DEVICES) with open(KNOWN_DEV_PATH, 'w') as fil: fil.write('device,name,track,picture\n') @@ -42,7 +37,8 @@ def setUpModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name """ Stops the Home Assistant server. """ - os.remove(KNOWN_DEV_PATH) + os.remove(os.path.join(get_test_config_dir(), + device_tracker.YAML_DEVICES)) class TestDeviceSunLightTrigger(unittest.TestCase): @@ -59,15 +55,16 @@ class TestDeviceSunLightTrigger(unittest.TestCase): loader.get_component('light.test').init() - device_tracker.setup(self.hass, { + self.assertTrue(device_tracker.setup(self.hass, { device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} - }) + })) - light.setup(self.hass, { + self.assertTrue(light.setup(self.hass, { light.DOMAIN: {CONF_PLATFORM: 'test'} - }) + })) - sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + self.assertTrue(sun.setup( + self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}})) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -76,8 +73,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase): def test_lights_on_when_sun_sets(self): """ Test lights go on when there is someone home and the sun sets. """ - device_sun_light_trigger.setup( - self.hass, {device_sun_light_trigger.DOMAIN: {}}) + self.assertTrue(device_sun_light_trigger.setup( + self.hass, {device_sun_light_trigger.DOMAIN: {}})) ensure_sun_risen(self.hass) @@ -97,12 +94,11 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass.pool.block_till_done() - device_sun_light_trigger.setup( - self.hass, {device_sun_light_trigger.DOMAIN: {}}) + self.assertTrue(device_sun_light_trigger.setup( + self.hass, {device_sun_light_trigger.DOMAIN: {}})) - self.scanner.leave_home('DEV1') - - trigger_device_tracker_scan(self.hass) + self.hass.states.set(device_tracker.ENTITY_ID_ALL_DEVICES, + STATE_NOT_HOME) self.hass.pool.block_till_done() @@ -116,11 +112,11 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass.pool.block_till_done() - device_sun_light_trigger.setup( - self.hass, {device_sun_light_trigger.DOMAIN: {}}) + self.assertTrue(device_sun_light_trigger.setup( + self.hass, {device_sun_light_trigger.DOMAIN: {}})) - self.scanner.come_home('DEV2') - trigger_device_tracker_scan(self.hass) + self.hass.states.set( + device_tracker.ENTITY_ID_FORMAT.format('device_2'), STATE_HOME) self.hass.pool.block_till_done() diff --git a/tests/components/test_device_tracker.py b/tests/components/test_device_tracker.py deleted file mode 100644 index 66fd97c4730..00000000000 --- a/tests/components/test_device_tracker.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -tests.test_component_device_tracker -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Tests the device tracker compoments. -""" -# pylint: disable=protected-access,too-many-public-methods -import unittest -from datetime import timedelta -import logging -import os - -import homeassistant.core as ha -import homeassistant.loader as loader -import homeassistant.util.dt as dt_util -from homeassistant.const import ( - STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM, - DEVICE_DEFAULT_NAME) -import homeassistant.components.device_tracker as device_tracker - -from tests.common import get_test_home_assistant - - -def setUpModule(): # pylint: disable=invalid-name - """ Setup to ignore group errors. """ - logging.disable(logging.CRITICAL) - - -class TestComponentsDeviceTracker(unittest.TestCase): - """ Tests homeassistant.components.device_tracker module. """ - - def setUp(self): # pylint: disable=invalid-name - """ Init needed objects. """ - self.hass = get_test_home_assistant() - loader.prepare(self.hass) - - self.known_dev_path = self.hass.config.path( - device_tracker.KNOWN_DEVICES_FILE) - - def tearDown(self): # pylint: disable=invalid-name - """ Stop down stuff we started. """ - self.hass.stop() - - if os.path.isfile(self.known_dev_path): - os.remove(self.known_dev_path) - - def test_is_on(self): - """ Test is_on method. """ - entity_id = device_tracker.ENTITY_ID_FORMAT.format('test') - - self.hass.states.set(entity_id, STATE_HOME) - - self.assertTrue(device_tracker.is_on(self.hass, entity_id)) - - self.hass.states.set(entity_id, STATE_NOT_HOME) - - self.assertFalse(device_tracker.is_on(self.hass, entity_id)) - - def test_setup(self): - """ Test setup method. """ - # Bogus config - self.assertFalse(device_tracker.setup(self.hass, {})) - - self.assertFalse( - device_tracker.setup(self.hass, {device_tracker.DOMAIN: {}})) - - # Test with non-existing component - self.assertFalse(device_tracker.setup( - self.hass, {device_tracker.DOMAIN: {CONF_PLATFORM: 'nonexisting'}} - )) - - # Test with a bad known device file around - with open(self.known_dev_path, 'w') as fil: - fil.write("bad data\nbad data\n") - - self.assertFalse(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} - })) - - def test_writing_known_devices_file(self): - """ Test the device tracker class. """ - scanner = loader.get_component( - 'device_tracker.test').get_scanner(None, None) - - scanner.reset() - - scanner.come_home('DEV1') - scanner.come_home('DEV2') - - self.assertTrue(device_tracker.setup(self.hass, { - device_tracker.DOMAIN: {CONF_PLATFORM: 'test'} - })) - - # Ensure a new known devices file has been created. - # Since the device_tracker uses a set internally we cannot - # know what the order of the devices in the known devices file is. - # To ensure all the three expected lines are there, we sort the file - with open(self.known_dev_path) as fil: - self.assertEqual( - ['DEV1,{},0,\n'.format(DEVICE_DEFAULT_NAME), 'DEV2,dev2,0,\n', - 'device,name,track,picture\n'], - sorted(fil)) - - # Write one where we track dev1, dev2 - with open(self.known_dev_path, 'w') as fil: - fil.write('device,name,track,picture\n') - fil.write('DEV1,device 1,1,http://example.com/dev1.jpg\n') - fil.write('DEV2,device 2,1,http://example.com/dev2.jpg\n') - - scanner.leave_home('DEV1') - scanner.come_home('DEV3') - - self.hass.services.call( - device_tracker.DOMAIN, - device_tracker.SERVICE_DEVICE_TRACKER_RELOAD) - - self.hass.pool.block_till_done() - - dev1 = device_tracker.ENTITY_ID_FORMAT.format('device_1') - dev2 = device_tracker.ENTITY_ID_FORMAT.format('device_2') - dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3') - - now = dt_util.utcnow() - - # Device scanner scans every 12 seconds. We need to sync our times to - # be every 12 seconds or else the time_changed event will be ignored. - nowAlmostMinimumGone = now + device_tracker.TIME_DEVICE_NOT_FOUND - nowAlmostMinimumGone -= timedelta( - seconds=12+(nowAlmostMinimumGone.second % 12)) - - nowMinimumGone = now + device_tracker.TIME_DEVICE_NOT_FOUND - nowMinimumGone += timedelta(seconds=12-(nowMinimumGone.second % 12)) - - # Test initial is correct - self.assertTrue(device_tracker.is_on(self.hass)) - self.assertFalse(device_tracker.is_on(self.hass, dev1)) - self.assertTrue(device_tracker.is_on(self.hass, dev2)) - self.assertIsNone(self.hass.states.get(dev3)) - - self.assertEqual( - 'http://example.com/dev1.jpg', - self.hass.states.get(dev1).attributes.get(ATTR_ENTITY_PICTURE)) - self.assertEqual( - 'http://example.com/dev2.jpg', - self.hass.states.get(dev2).attributes.get(ATTR_ENTITY_PICTURE)) - - # Test if dev3 got added to known dev file - with open(self.known_dev_path) as fil: - self.assertEqual('DEV3,dev3,0,\n', list(fil)[-1]) - - # Change dev3 to track - with open(self.known_dev_path, 'w') as fil: - fil.write("device,name,track,picture\n") - fil.write('DEV1,Device 1,1,http://example.com/picture.jpg\n') - fil.write('DEV2,Device 2,1,http://example.com/picture.jpg\n') - fil.write('DEV3,DEV3,1,\n') - - scanner.come_home('DEV1') - scanner.leave_home('DEV2') - - # reload dev file - self.hass.services.call( - device_tracker.DOMAIN, - device_tracker.SERVICE_DEVICE_TRACKER_RELOAD) - - self.hass.pool.block_till_done() - - # Test what happens if a device comes home and another leaves - self.assertTrue(device_tracker.is_on(self.hass)) - self.assertTrue(device_tracker.is_on(self.hass, dev1)) - # Dev2 will still be home because of the error margin on time - self.assertTrue(device_tracker.is_on(self.hass, dev2)) - # dev3 should be tracked now after we reload the known devices - self.assertTrue(device_tracker.is_on(self.hass, dev3)) - - self.assertIsNone( - self.hass.states.get(dev3).attributes.get(ATTR_ENTITY_PICTURE)) - - # Test if device leaves what happens, test the time span - self.hass.bus.fire( - ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowAlmostMinimumGone}) - - self.hass.pool.block_till_done() - - self.assertTrue(device_tracker.is_on(self.hass)) - self.assertTrue(device_tracker.is_on(self.hass, dev1)) - # Dev2 will still be home because of the error time - self.assertTrue(device_tracker.is_on(self.hass, dev2)) - self.assertTrue(device_tracker.is_on(self.hass, dev3)) - - # Now test if gone for longer then error margin - self.hass.bus.fire( - ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowMinimumGone}) - - self.hass.pool.block_till_done() - - self.assertTrue(device_tracker.is_on(self.hass)) - self.assertTrue(device_tracker.is_on(self.hass, dev1)) - self.assertFalse(device_tracker.is_on(self.hass, dev2)) - self.assertTrue(device_tracker.is_on(self.hass, dev3)) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 65fcb5b6091..e8c0c53d13e 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -7,6 +7,7 @@ Tests Home Assistant HTTP component does what it should do. # pylint: disable=protected-access,too-many-public-methods import re import unittest +from unittest.mock import patch import requests @@ -34,7 +35,9 @@ def _url(path=""): return HTTP_BASE_URL + path -def setUpModule(): # pylint: disable=invalid-name +@patch('homeassistant.components.http.util.get_local_ip', + return_value='127.0.0.1') +def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name """ Initalizes a Home Assistant server. """ global hass diff --git a/tests/components/test_group.py b/tests/components/test_group.py index d66a24606a3..d7ed7f105d0 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -199,7 +199,7 @@ class TestComponentsGroup(unittest.TestCase): self.hass, { group.DOMAIN: { - 'second_group': self.group_entity_id + ',light.Bowl' + 'second_group': 'light.Bowl, ' + self.group_entity_id } })) @@ -207,6 +207,8 @@ class TestComponentsGroup(unittest.TestCase): group.ENTITY_ID_FORMAT.format('second_group')) self.assertEqual(STATE_ON, group_state.state) + self.assertEqual(set((self.group_entity_id, 'light.bowl')), + set(group_state.attributes['entity_id'])) self.assertFalse(group_state.attributes[group.ATTR_AUTO]) def test_groups_get_unique_names(self): diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 12d10c52744..fdd8270a661 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -8,6 +8,8 @@ Tests the history component. import time import os import unittest +from unittest.mock import patch +from datetime import timedelta import homeassistant.core as ha import homeassistant.util.dt as dt_util @@ -68,11 +70,7 @@ class TestComponentHistory(unittest.TestCase): self.init_recorder() states = [] - # Create 10 states for 5 different entities - # After the first 5, sleep a second and save the time - # history.get_states takes the latest states BEFORE point X - - for i in range(10): + for i in range(5): state = ha.State( 'test.point_in_time_{}'.format(i % 5), "State {}".format(i), @@ -80,19 +78,27 @@ class TestComponentHistory(unittest.TestCase): mock_state_change_event(self.hass, state) self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - if i < 5: - states.append(state) + states.append(state) - if i == 4: - time.sleep(1) - point = dt_util.utcnow() + recorder._INSTANCE.block_till_done() - self.assertEqual( - states, - sorted( - history.get_states(point), key=lambda state: state.entity_id)) + point = dt_util.utcnow() + timedelta(seconds=1) + + with patch('homeassistant.util.dt.utcnow', return_value=point): + for i in range(5): + state = ha.State( + 'test.point_in_time_{}'.format(i % 5), + "State {}".format(i), + {'attribute_test': i}) + + mock_state_change_event(self.hass, state) + self.hass.pool.block_till_done() + + # Get states returns everything before POINT + self.assertEqual(states, + sorted(history.get_states(point), + key=lambda state: state.entity_id)) # Test get_state here because we have a DB setup self.assertEqual( @@ -113,22 +119,20 @@ class TestComponentHistory(unittest.TestCase): set_state('YouTube') start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) - time.sleep(1) + with patch('homeassistant.util.dt.utcnow', return_value=point): + states = [ + set_state('idle'), + set_state('Netflix'), + set_state('Plex'), + set_state('YouTube'), + ] - states = [ - set_state('idle'), - set_state('Netflix'), - set_state('Plex'), - set_state('YouTube'), - ] - - time.sleep(1) - - end = dt_util.utcnow() - - set_state('Netflix') - set_state('Plex') + with patch('homeassistant.util.dt.utcnow', return_value=end): + set_state('Netflix') + set_state('Plex') self.assertEqual( {entity_id: states}, diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 0074b75e148..4ff334c1b1e 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -20,7 +20,6 @@ class TestComponentsCore(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ self.hass = ha.HomeAssistant() - loader.prepare(self.hass) self.assertTrue(comps.setup(self.hass, {})) self.hass.states.set('light.Bowl', STATE_ON) diff --git a/tests/components/test_light.py b/tests/components/test_light.py index e56dcbb02ad..515b79b6fc0 100644 --- a/tests/components/test_light.py +++ b/tests/components/test_light.py @@ -23,7 +23,6 @@ class TestLight(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant() - loader.prepare(self.hass) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ diff --git a/tests/components/test_media_player.py b/tests/components/test_media_player.py index 1fd406dc026..28d39206c47 100644 --- a/tests/components/test_media_player.py +++ b/tests/components/test_media_player.py @@ -5,7 +5,6 @@ tests.test_component_media_player Tests media_player component. """ # pylint: disable=too-many-public-methods,protected-access -import logging import unittest import homeassistant.core as ha @@ -18,11 +17,6 @@ import homeassistant.components.media_player as media_player from tests.common import mock_service -def setUpModule(): # pylint: disable=invalid-name - """ Setup to ignore media_player errors. """ - logging.disable(logging.CRITICAL) - - class TestMediaPlayer(unittest.TestCase): """ Test the media_player module. """ diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 9d2ae38fdd6..366b483d3ff 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -77,7 +77,7 @@ class TestSun(unittest.TestCase): """ Test if the state changes at next setting/rising. """ self.hass.config.latitude = '32.87336' self.hass.config.longitude = '117.22743' - sun.setup(self.hass, {}) + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) if sun.is_on(self.hass): test_state = sun.STATE_BELOW_HORIZON diff --git a/tests/components/test_switch.py b/tests/components/test_switch.py index 642c7f45aa9..dc7129ca541 100644 --- a/tests/components/test_switch.py +++ b/tests/components/test_switch.py @@ -7,9 +7,9 @@ Tests switch component. # pylint: disable=too-many-public-methods,protected-access import unittest -import homeassistant.loader as loader +from homeassistant import loader +from homeassistant.components import switch from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -import homeassistant.components.switch as switch from tests.common import get_test_home_assistant @@ -19,7 +19,6 @@ class TestSwitch(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant() - loader.prepare(self.hass) platform = loader.get_component('switch.test') diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index b8823f23a5a..cdca36a9701 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -34,14 +34,6 @@ class TestHelpersEntity(unittest.TestCase): ATTR_HIDDEN, self.hass.states.get(self.entity.entity_id).attributes) - def test_setting_hidden_to_true(self): - self.entity.hidden = True - self.entity.update_ha_state() - - state = self.hass.states.get(self.entity.entity_id) - - self.assertTrue(state.attributes.get(ATTR_HIDDEN)) - def test_overwriting_hidden_property_to_true(self): """ Test we can overwrite hidden property to True. """ entity.Entity.overwrite_attribute(self.entity.entity_id, @@ -50,14 +42,3 @@ class TestHelpersEntity(unittest.TestCase): state = self.hass.states.get(self.entity.entity_id) self.assertTrue(state.attributes.get(ATTR_HIDDEN)) - - def test_overwriting_hidden_property_to_false(self): - """ Test we can overwrite hidden property to True. """ - entity.Entity.overwrite_attribute(self.entity.entity_id, - [ATTR_HIDDEN], [False]) - self.entity.hidden = True - self.entity.update_ha_state() - - self.assertNotIn( - ATTR_HIDDEN, - self.hass.states.get(self.entity.entity_id).attributes) diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index c1af6ba8ccc..0e7c310d91f 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -7,13 +7,13 @@ Tests component helpers. # pylint: disable=protected-access,too-many-public-methods import unittest -from common import get_test_home_assistant - import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import extract_entity_ids +from tests.common import get_test_home_assistant + class TestComponentsCore(unittest.TestCase): """ Tests homeassistant.components module. """ @@ -21,7 +21,6 @@ class TestComponentsCore(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ self.hass = get_test_home_assistant() - loader.prepare(self.hass) self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Ceiling', STATE_OFF) diff --git a/tests/test_config.py b/tests/test_config.py index f683fac890c..65c93f9f333 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) -from common import get_test_config_dir, mock_detect_location_info +from tests.common import get_test_config_dir, mock_detect_location_info CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -49,13 +49,15 @@ class TestConfig(unittest.TestCase): self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR)) - def test_ensure_config_exists_creates_config(self): + @mock.patch('builtins.print') + def test_ensure_config_exists_creates_config(self, mock_print): """ Test that calling ensure_config_exists creates a new config file if none exists. """ config_util.ensure_config_exists(CONFIG_DIR, False) self.assertTrue(os.path.isfile(YAML_PATH)) + self.assertTrue(mock_print.called) def test_ensure_config_exists_uses_existing_config(self): """ Test that calling ensure_config_exists uses existing config. """ @@ -100,11 +102,12 @@ class TestConfig(unittest.TestCase): self.assertEqual({'hello': 'world'}, config_util.load_config_file(YAML_PATH)) - def test_create_default_config_detect_location(self): + @mock.patch('homeassistant.util.location.detect_location_info', + mock_detect_location_info) + @mock.patch('builtins.print') + def test_create_default_config_detect_location(self, mock_print): """ Test that detect location sets the correct config keys. """ - with mock.patch('homeassistant.util.location.detect_location_info', - mock_detect_location_info): - config_util.ensure_config_exists(CONFIG_DIR) + config_util.ensure_config_exists(CONFIG_DIR) config = config_util.load_config_file(YAML_PATH) @@ -121,11 +124,15 @@ class TestConfig(unittest.TestCase): } self.assertEqual(expected_values, ha_conf) + self.assertTrue(mock_print.called) - def test_create_default_config_returns_none_if_write_error(self): + @mock.patch('builtins.print') + def test_create_default_config_returns_none_if_write_error(self, + mock_print): """ Test that writing default config to non existing folder returns None. """ self.assertIsNone( config_util.create_default_config( os.path.join(CONFIG_DIR, 'non_existing_dir/'), False)) + self.assertTrue(mock_print.called) diff --git a/tests/test_core.py b/tests/test_core.py index 6e7b52795b2..30ef03ac1b4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,14 +8,16 @@ Provides tests to verify that Home Assistant core works. # pylint: disable=too-few-public-methods import os import unittest -import unittest.mock as mock +from unittest.mock import patch import time import threading -from datetime import datetime +from datetime import datetime, timedelta 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 @@ -53,29 +55,26 @@ class TestHomeAssistant(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(calls)) + # @patch('homeassistant.core.time.sleep') def test_block_till_stoped(self): """ Test if we can block till stop service is called. """ - blocking_thread = threading.Thread(target=self.hass.block_till_stopped) + with patch('time.sleep'): + blocking_thread = threading.Thread( + target=self.hass.block_till_stopped) - self.assertFalse(blocking_thread.is_alive()) + self.assertFalse(blocking_thread.is_alive()) - blocking_thread.start() + blocking_thread.start() - # Threads are unpredictable, try 20 times if we're ready - wait_loops = 0 - while not blocking_thread.is_alive() and wait_loops < 20: - wait_loops += 1 - time.sleep(0.05) + self.assertTrue(blocking_thread.is_alive()) - self.assertTrue(blocking_thread.is_alive()) + self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) + self.hass.pool.block_till_done() - self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) - self.hass.pool.block_till_done() - - # Threads are unpredictable, try 20 times if we're ready - wait_loops = 0 - while blocking_thread.is_alive() and wait_loops < 20: - wait_loops += 1 + # Wait for thread to stop + for _ in range(20): + if not blocking_thread.is_alive(): + break time.sleep(0.05) self.assertFalse(blocking_thread.is_alive()) @@ -86,13 +85,9 @@ class TestHomeAssistant(unittest.TestCase): lambda event: calls.append(1)) def raise_keyboardinterrupt(length): - # We don't want to patch the sleep of the timer. - if length == 1: - raise KeyboardInterrupt + raise KeyboardInterrupt - self.hass.start() - - with mock.patch('time.sleep', raise_keyboardinterrupt): + with patch('homeassistant.core.time.sleep', raise_keyboardinterrupt): self.hass.block_till_stopped() self.assertEqual(1, len(calls)) @@ -250,7 +245,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): @@ -398,9 +393,10 @@ class TestStateMachine(unittest.TestCase): def test_last_changed_not_updated_on_same_state(self): state = self.states.get('light.Bowl') - time.sleep(1) + future = dt_util.utcnow() + timedelta(hours=10) - self.states.set("light.Bowl", "on") + with patch('homeassistant.util.dt.utcnow', return_value=future): + self.states.set("light.Bowl", "on", {'attr': 'triggers_change'}) self.assertEqual(state.last_changed, self.states.get('light.Bowl').last_changed) @@ -489,18 +485,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_loader.py b/tests/test_loader.py index 03bd7e7419c..124a5c87d16 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,14 +10,13 @@ import unittest import homeassistant.loader as loader import homeassistant.components.http as http -from common import get_test_home_assistant, MockModule +from tests.common import get_test_home_assistant, MockModule class TestLoader(unittest.TestCase): """ Test the loader module. """ def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant() - loader.prepare(self.hass) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -25,9 +24,9 @@ class TestLoader(unittest.TestCase): def test_set_component(self): """ Test if set_component works. """ - loader.set_component('switch.test', http) + loader.set_component('switch.test_set', http) - self.assertEqual(http, loader.get_component('switch.test')) + self.assertEqual(http, loader.get_component('switch.test_set')) def test_get_component(self): """ Test if get_component works. """ diff --git a/tests/test_remote.py b/tests/test_remote.py index e5bfd71199f..31ccad8f7aa 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -8,6 +8,7 @@ Uses port 8125 as a port that nothing runs on """ # pylint: disable=protected-access,too-many-public-methods import unittest +from unittest.mock import patch import homeassistant.core as ha import homeassistant.bootstrap as bootstrap @@ -29,7 +30,9 @@ def _url(path=""): return HTTP_BASE_URL + path -def setUpModule(): # pylint: disable=invalid-name +@patch('homeassistant.components.http.util.get_local_ip', + return_value='127.0.0.1') +def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name """ Initalizes a Home Assistant server and Slave instance. """ global hass, slave, master_api, broken_api @@ -51,6 +54,10 @@ def setUpModule(): # pylint: disable=invalid-name # Start slave slave = remote.HomeAssistant(master_api) + bootstrap.setup_component( + slave, http.DOMAIN, + {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: 8130}}) slave.start() diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 5a4fb44b2d4..8b5f115d03b 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -6,10 +6,11 @@ Tests Home Assistant util methods. """ # pylint: disable=too-many-public-methods import unittest -import time +from unittest.mock import patch from datetime import datetime, timedelta -import homeassistant.util as util +from homeassistant import util +import homeassistant.util.dt as dt_util class TestUtil(unittest.TestCase): @@ -169,21 +170,19 @@ class TestUtil(unittest.TestCase): def test_throttle(self): """ Test the add cooldown decorator. """ calls1 = [] + calls2 = [] - @util.Throttle(timedelta(milliseconds=500)) + @util.Throttle(timedelta(seconds=4)) def test_throttle1(): calls1.append(1) - calls2 = [] - - @util.Throttle( - timedelta(milliseconds=500), timedelta(milliseconds=250)) + @util.Throttle(timedelta(seconds=4), timedelta(seconds=2)) def test_throttle2(): calls2.append(1) - # Ensure init is ok - self.assertEqual(0, len(calls1)) - self.assertEqual(0, len(calls2)) + now = dt_util.utcnow() + plus3 = now + timedelta(seconds=3) + plus5 = plus3 + timedelta(seconds=2) # Call first time and ensure methods got called test_throttle1() @@ -206,25 +205,16 @@ class TestUtil(unittest.TestCase): self.assertEqual(2, len(calls1)) self.assertEqual(1, len(calls2)) - # Sleep past the no throttle interval for throttle2 - time.sleep(.3) - - test_throttle1() - test_throttle2() + with patch('homeassistant.util.utcnow', return_value=plus3): + test_throttle1() + test_throttle2() self.assertEqual(2, len(calls1)) self.assertEqual(1, len(calls2)) - test_throttle1(no_throttle=True) - test_throttle2(no_throttle=True) + with patch('homeassistant.util.utcnow', return_value=plus5): + test_throttle1() + test_throttle2() self.assertEqual(3, len(calls1)) self.assertEqual(2, len(calls2)) - - time.sleep(.5) - - test_throttle1() - test_throttle2() - - self.assertEqual(4, len(calls1)) - self.assertEqual(3, len(calls2))