diff --git a/.coveragerc b/.coveragerc index 2ea0f11a6bd..f0953f49cd5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -24,6 +24,9 @@ omit = homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py + homeassistant/components/light/vera.py + homeassistant/components/sensor/vera.py + homeassistant/components/switch/vera.py [report] diff --git a/.gitmodules b/.gitmodules index 27b3cae6bf8..ae38be7c61b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,6 +13,9 @@ [submodule "homeassistant/components/frontend/www_static/polymer/home-assistant-js"] path = homeassistant/components/frontend/www_static/polymer/home-assistant-js url = https://github.com/balloob/home-assistant-js.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 + path = homeassistant/external/nzbclients + url = https://github.com/jamespcole/home-assistant-nzb-clients.git diff --git a/Dockerfile b/Dockerfile index a78322bbd80..ff34dc79297 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ MAINTAINER Paulus Schoutsen VOLUME /config RUN apt-get update && \ - apt-get install -y cython3 libudev-dev python-sphinx python3-setuptools mercurial && \ + apt-get install -y cython3 libudev-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ pip3 install cython && \ scripts/build_python_openzwave diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 51217c090bb..6c0d9f64a1f 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -101,7 +101,8 @@ automation 2: time_seconds: 0 execute_service: notify.notify - service_data: {"message":"It's 4, time for beer!"} + service_data: + message: It's 4, time for beer! sensor: platform: systemmonitor @@ -119,4 +120,31 @@ sensor: - type: 'memory_free' - type: 'processor_use' - type: 'process' - arg: 'octave-cli' \ No newline at end of file + arg: 'octave-cli' + +script: + # Turns on the bedroom lights and then the living room lights 1 minute later + wakeup: + alias: Wake Up + sequence: + # alias is optional + - alias: Bedroom lights on + execute_service: light.turn_on + service_data: + entity_id: group.bedroom + - delay: + # supports seconds, milliseconds, minutes, hours, etc. + minutes: 1 + - alias: Living room lights on + execute_service: light.turn_on + service_data: + entity_id: group.living_room + +scene: + - name: Romantic + entities: + light.tv_back_light: on + light.ceiling: + state: on + color: [0.33, 0.66] + brightness: 200 diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 354898c0319..bc6dca95113 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -115,6 +115,7 @@ class HomeAssistant(object): action(now) self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) + return point_in_time_listener # pylint: disable=too-many-arguments def track_time_change(self, action, @@ -154,6 +155,7 @@ class HomeAssistant(object): action(event.data[ATTR_NOW]) self.bus.listen(EVENT_TIME_CHANGED, time_listener) + return time_listener def stop(self): """ Stops Home Assistant and shuts down all threads. """ @@ -457,6 +459,11 @@ class State(object): self.last_changed = util.strip_microseconds( last_changed or self.last_updated) + @property + def domain(self): + """ Returns domain of this state. """ + return util.split_entity_id(self.entity_id)[0] + def copy(self): """ Creates a copy of itself. """ return State(self.entity_id, self.state, diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 56d77fdcb26..f9a90664ae7 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -119,6 +119,11 @@ def from_config_file(config_path, hass=None): if os.path.splitext(config_path)[1] == '.yaml': # Read yaml config_dict = yaml.load(io.open(config_path, 'r')) + + # If YAML file was empty + if config_dict is None: + config_dict = {} + else: # Read config config = configparser.ConfigParser() diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index a29b81e3cba..ebb632b95ab 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -10,7 +10,7 @@ import threading import json import homeassistant as ha -from homeassistant.helpers import TrackStates +from homeassistant.helpers.state import TrackStates import homeassistant.remote as rem from homeassistant.const import ( URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 33eef9fe3dc..21bea96201b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -5,11 +5,10 @@ homeassistant.components.automation Allows to setup simple automation rules via the config file. """ import logging -import json from homeassistant.loader import get_component from homeassistant.helpers import config_per_platform -from homeassistant.util import convert, split_entity_id +from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID DOMAIN = "automation" @@ -54,8 +53,7 @@ def _get_action(hass, config): if CONF_SERVICE in config: domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = convert( - config.get(CONF_SERVICE_DATA), json.loads, {}) + service_data = config.get(CONF_SERVICE_DATA, {}) if not isinstance(service_data, dict): _LOGGER.error( diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 94e1bcc805f..8a78f20d485 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -5,8 +5,6 @@ homeassistant.components.automation.event Offers event listening automation rules. """ import logging -import json -from homeassistant.util import convert CONF_EVENT_TYPE = "event_type" CONF_EVENT_DATA = "event_data" @@ -22,7 +20,7 @@ def register(hass, config, action): _LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE) return False - event_data = convert(config.get(CONF_EVENT_DATA), json.loads, {}) + event_data = config.get(CONF_EVENT_DATA, {}) def handle_event(event): """ Listens for events and calls the action when data matches. """ diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py new file mode 100644 index 00000000000..bf78e13a094 --- /dev/null +++ b/homeassistant/components/conversation.py @@ -0,0 +1,72 @@ +""" +homeassistant.components.conversation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to have conversations with Home Assistant. +This is more a proof of concept. +""" +import logging +import re + +import homeassistant +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) + +DOMAIN = "conversation" +DEPENDENCIES = [] + +SERVICE_PROCESS = "process" + +ATTR_TEXT = "text" + +REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') + + +def setup(hass, config): + """ Registers the process service. """ + logger = logging.getLogger(__name__) + + def process(service): + """ Parses text into commands for Home Assistant. """ + if ATTR_TEXT not in service.data: + logger.error("Received process service call without a text") + return + + text = service.data[ATTR_TEXT].lower() + + match = REGEX_TURN_COMMAND.match(text) + + if not match: + logger.error("Unable to process: %s", text) + return + + name, command = match.groups() + + entity_ids = [ + state.entity_id for state in hass.states.all() + if state.attributes.get(ATTR_FRIENDLY_NAME, "").lower() == name] + + if not entity_ids: + logger.error( + "Could not find entity id %s from text %s", name, text) + return + + if command == 'on': + hass.services.call( + homeassistant.DOMAIN, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity_ids, + }, blocking=True) + + elif command == 'off': + hass.services.call( + homeassistant.DOMAIN, SERVICE_TURN_OFF, { + ATTR_ENTITY_ID: entity_ids, + }, blocking=True) + + else: + logger.error( + 'Got unsupported command %s from text %s', command, text) + + hass.services.register(DOMAIN, SERVICE_PROCESS, process) + + return True diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 8d52210dd23..53774210868 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -10,7 +10,7 @@ import homeassistant as ha import homeassistant.bootstrap as bootstrap import homeassistant.loader as loader from homeassistant.const import ( - CONF_PLATFORM, ATTR_ENTITY_PICTURE, STATE_ON, + CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE) DOMAIN = "demo" @@ -18,7 +18,7 @@ DOMAIN = "demo" DEPENDENCIES = [] COMPONENTS_WITH_DEMO_PLATFORM = [ - 'switch', 'light', 'thermostat', 'sensor', 'media_player'] + 'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify'] def setup(hass, config): @@ -29,16 +29,12 @@ def setup(hass, config): config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) - if config[DOMAIN].get('hide_demo_state') != '1': + if config[DOMAIN].get('hide_demo_state') != 1: hass.states.set('a.Demo_Mode', 'Enabled') # Setup sun - if CONF_LATITUDE not in config[ha.DOMAIN]: - config[ha.DOMAIN][CONF_LATITUDE] = '32.87336' - - if CONF_LONGITUDE not in config[ha.DOMAIN]: - config[ha.DOMAIN][CONF_LONGITUDE] = '-117.22743' - + config[ha.DOMAIN].setdefault(CONF_LATITUDE, '32.87336') + config[ha.DOMAIN].setdefault(CONF_LONGITUDE, '-117.22743') loader.get_component('sun').setup(hass, config) # Setup demo platforms @@ -52,21 +48,57 @@ def setup(hass, config): group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0]]) group.setup_group(hass, 'bedroom', [lights[2], switches[1]]) - # Setup process - hass.states.set("process.XBMC", STATE_ON) + # Setup scripts + bootstrap.setup_component( + hass, 'script', + {'script': { + 'demo': { + 'alias': 'Demo {}'.format(lights[0]), + 'sequence': [{ + 'execute_service': 'light.turn_off', + 'service_data': {ATTR_ENTITY_ID: lights[0]} + }, { + 'delay': {'seconds': 5} + }, { + 'execute_service': 'light.turn_on', + 'service_data': {ATTR_ENTITY_ID: lights[0]} + }, { + 'delay': {'seconds': 5} + }, { + 'execute_service': 'light.turn_off', + 'service_data': {ATTR_ENTITY_ID: lights[0]} + }] + }}}) - # Setup device tracker - hass.states.set("device_tracker.Paulus", "home", + # Setup scenes + bootstrap.setup_component( + hass, 'scene', + {'scene': [ + {'name': 'Romantic lights', + 'entities': { + lights[0]: True, + lights[1]: {'state': 'on', 'xy_color': [0.33, 0.66], + 'brightness': 200}, + }}, + {'name': 'Switch on and off', + 'entities': { + switches[0]: True, + switches[1]: False, + }}, + ]}) + + # Setup fake device tracker + hass.states.set("device_tracker.paulus", "home", {ATTR_ENTITY_PICTURE: "http://graph.facebook.com/schoutsen/picture"}) - hass.states.set("device_tracker.Anne_Therese", "not_home", + hass.states.set("device_tracker.anne_therese", "not_home", {ATTR_ENTITY_PICTURE: "http://graph.facebook.com/anne.t.frederiksen/picture"}) hass.states.set("group.all_devices", "home", { "auto": True, - "entity_id": [ + ATTR_ENTITY_ID: [ "device_tracker.Paulus", "device_tracker.Anne_Therese" ] diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 8def4079d05..4740336767f 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -153,7 +153,7 @@ def setup(hass, config): logger.info( "Everyone has left but there are lights on. Turning them off") - light.turn_off(hass) + light.turn_off(hass, light_ids) # Track home coming of each device hass.states.track_change( diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 4ab72eaba6b..85e8add47fc 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -236,7 +236,7 @@ class DeviceTracker(object): try: for row in csv.DictReader(inp): - device = row['device'] + device = row['device'].upper() if row['track'] == '1': if device in self.tracked: diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index a4fd5f6cfff..860ba3b45fb 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -1,6 +1,6 @@ """ Supports scanning using nmap. """ import logging -from datetime import timedelta +from datetime import timedelta, datetime import threading from collections import namedtuple import subprocess @@ -11,7 +11,7 @@ from libnmap.parser import NmapParser, NmapParserException from homeassistant.const import CONF_HOSTS 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 @@ -19,6 +19,9 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +# interval in minutes to exclude devices from a scan while they are home +CONF_HOME_INTERVAL = "home_interval" + def get_scanner(hass, config): """ Validates config and returns a Nmap scanner. """ @@ -30,7 +33,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple("Device", ["mac", "name"]) +Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) def _arp(ip_address): @@ -53,6 +56,8 @@ class NmapDeviceScanner(object): self.lock = threading.Lock() self.hosts = config[CONF_HOSTS] + minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) + self.home_interval = timedelta(minutes=minutes) self.success_init = True self._update_info() @@ -77,6 +82,33 @@ 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 = datetime.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, 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. @@ -87,35 +119,24 @@ class NmapDeviceScanner(object): with self.lock: _LOGGER.info("Scanning") - nmap = NmapProcess(targets=self.hosts, options="-F") + options = "-F" + exclude_targets = set() + if self.home_interval: + now = datetime.now() + for host in self.last_results: + if host.last_update + self.home_interval > now: + exclude_targets.add(host) + if len(exclude_targets) > 0: + 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: - try: - results = NmapParser.parse(nmap.stdout) - 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, name) - 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 - + if self._parse_results(nmap.stdout): + self.last_results.extend(exclude_targets) else: self.last_results = [] _LOGGER.error(nmap.stderr) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 3886b2c2581..f580952cfeb 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 = "1c265f0f07e6038c2cbb9b277e58b994" +VERSION = "a063d1482fd49e9297d64e1329324f1c" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index d9a004de623..aaf95520da6 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -122,9 +122,11 @@ b.events&&Object.keys(a).length>0&&console.log("[%s] addHostListeners:",this.loc background-color: #039be5; } - + - + - + diff --git a/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html b/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html index 78b808dfb09..195a9cb1109 100644 --- a/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html +++ b/homeassistant/components/frontend/www_static/polymer/cards/state-card-configurator.html @@ -1,23 +1,11 @@ +