diff --git a/.coveragerc b/.coveragerc index adb3c59765e..75f4cbd20fd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,9 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/insteon_hub.py + homeassistant/components/*/insteon_hub.py + homeassistant/components/isy994.py homeassistant/components/*/isy994.py @@ -32,6 +35,9 @@ omit = homeassistant/components/wink.py homeassistant/components/*/wink.py + homeassistant/components/zigbee.py + homeassistant/components/*/zigbee.py + homeassistant/components/zwave.py homeassistant/components/*/zwave.py @@ -41,6 +47,9 @@ omit = homeassistant/components/mysensors.py homeassistant/components/*/mysensors.py + homeassistant/components/nest.py + homeassistant/components/*/nest.py + homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py @@ -57,7 +66,6 @@ omit = homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py - homeassistant/components/device_tracker/owntracks.py homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py @@ -66,11 +74,13 @@ omit = homeassistant/components/discovery.py homeassistant/components/downloader.py homeassistant/components/ifttt.py + homeassistant/components/statsd.py homeassistant/components/influxdb.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py + homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py @@ -91,7 +101,9 @@ omit = homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py + homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py + homeassistant/components/notify/googlevoice.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/cpuspeed.py @@ -102,6 +114,7 @@ omit = homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/netatmo.py + homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py @@ -124,7 +137,6 @@ omit = homeassistant/components/thermostat/heatmiser.py homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/honeywell.py - homeassistant/components/thermostat/nest.py homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py diff --git a/.gitignore b/.gitignore index 3ee71808ab1..1e79f07b663 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,16 @@ nosetests.xml .python-version +# emacs auto backups +*~ +*# +*.orig + # venv stuff pyvenv.cfg pip-selfcheck.json +venv + +# vimmy stuff +*.swp +*.swo diff --git a/config/custom_components/example.py b/config/custom_components/example.py index ee7f18f437a..08b3f4c2a83 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -29,9 +29,13 @@ import time import logging from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF -import homeassistant.loader as loader from homeassistant.helpers import validate_config +from homeassistant.helpers.event_decorators import \ + track_state_change, track_time_change +from homeassistant.helpers.service import service import homeassistant.components as core +from homeassistant.components import device_tracker +from homeassistant.components import light # The domain of your component. Should be equal to the name of your component DOMAIN = "example" @@ -39,11 +43,14 @@ DOMAIN = "example" # List of component names (string) your component depends upon # We depend on group because group will be loaded after all the components that # initialize devices have been setup. -DEPENDENCIES = ['group'] +DEPENDENCIES = ['group', 'device_tracker', 'light'] # Configuration key for the entity id we are targetting CONF_TARGET = 'target' +# Variable for storing configuration parameters +TARGET_ID = None + # Name of the service that we expose SERVICE_FLASH = 'flash' @@ -53,84 +60,89 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Setup example component. """ + global TARGET_ID # Validate that all required config options are given if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): return False - target_id = config[DOMAIN][CONF_TARGET] + TARGET_ID = config[DOMAIN][CONF_TARGET] # Validate that the target entity id exists - if hass.states.get(target_id) is None: - _LOGGER.error("Target entity id %s does not exist", target_id) + if hass.states.get(TARGET_ID) is None: + _LOGGER.error("Target entity id %s does not exist", + TARGET_ID) - # Tell the bootstrapper that we failed to initialize + # Tell the bootstrapper that we failed to initialize and clear the + # stored target id so our functions don't run. + TARGET_ID = None return False - # We will use the component helper methods to check the states. - device_tracker = loader.get_component('device_tracker') - light = loader.get_component('light') - - def track_devices(entity_id, old_state, new_state): - """ Called when the group.all devices change state. """ - - # If anyone comes home and the core is not on, turn it on. - if new_state.state == STATE_HOME and not core.is_on(hass, target_id): - - core.turn_on(hass, target_id) - - # If all people leave the house and the core is on, turn it off - elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): - - core.turn_off(hass, target_id) - - # Register our track_devices method to receive state changes of the - # all tracked devices group. - hass.states.track_change( - device_tracker.ENTITY_ID_ALL_DEVICES, track_devices) - - def wake_up(now): - """ Turn it on in the morning if there are people home and - it is not already on. """ - - if device_tracker.is_on(hass) and not core.is_on(hass, target_id): - _LOGGER.info('People home at 7AM, turning it on') - core.turn_on(hass, target_id) - - # Register our wake_up service to be called at 7AM in the morning - hass.track_time_change(wake_up, hour=7, minute=0, second=0) - - def all_lights_off(entity_id, old_state, new_state): - """ If all lights turn off, turn off. """ - - if core.is_on(hass, target_id): - _LOGGER.info('All lights have been turned off, turning it off') - core.turn_off(hass, target_id) - - # Register our all_lights_off method to be called when all lights turn off - hass.states.track_change( - light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF) - - def flash_service(call): - """ Service that will turn the target off for 10 seconds - if on and vice versa. """ - - if core.is_on(hass, target_id): - core.turn_off(hass, target_id) - - time.sleep(10) - - core.turn_on(hass, target_id) - - else: - core.turn_on(hass, target_id) - - time.sleep(10) - - core.turn_off(hass, target_id) - - # Register our service with HASS. - hass.services.register(DOMAIN, SERVICE_FLASH, flash_service) - - # Tells the bootstrapper that the component was successfully initialized + # Tell the bootstrapper that we initialized successfully return True + + +@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES) +def track_devices(hass, entity_id, old_state, new_state): + """ Called when the group.all devices change state. """ + # If the target id is not set, return + if not TARGET_ID: + return + + # If anyone comes home and the entity is not on, turn it on. + if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID): + + core.turn_on(hass, TARGET_ID) + + # If all people leave the house and the entity is on, turn it off + elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID): + + core.turn_off(hass, TARGET_ID) + + +@track_time_change(hour=7, minute=0, second=0) +def wake_up(hass, now): + """ + Turn it on in the morning (7 AM) if there are people home and + it is not already on. + """ + if not TARGET_ID: + return + + if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID): + _LOGGER.info('People home at 7AM, turning it on') + core.turn_on(hass, TARGET_ID) + + +@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF) +def all_lights_off(hass, entity_id, old_state, new_state): + """ If all lights turn off, turn off. """ + if not TARGET_ID: + return + + if core.is_on(hass, TARGET_ID): + _LOGGER.info('All lights have been turned off, turning it off') + core.turn_off(hass, TARGET_ID) + + +@service(DOMAIN, SERVICE_FLASH) +def flash_service(hass, call): + """ + Service that will turn the target off for 10 seconds if on and vice versa. + """ + if not TARGET_ID: + return + + if core.is_on(hass, TARGET_ID): + core.turn_off(hass, TARGET_ID) + + time.sleep(10) + + core.turn_on(hass, TARGET_ID) + + else: + core.turn_on(hass, TARGET_ID) + + time.sleep(10) + + core.turn_off(hass, TARGET_ID) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b704fc082ac..3f88c6f4388 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,6 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +from homeassistant.helpers import event_decorators, service from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -199,6 +200,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, _LOGGER.info('Home Assistant core initialized') + # give event decorators access to HASS + event_decorators.HASS = hass + service.HASS = hass + # Setup the components for domain in loader.load_order_components(components): _setup_component(hass, domain, config) @@ -223,7 +228,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False, enable_logging(hass, verbose, daemon, log_rotate_days) - config_dict = config_util.load_config_file(config_path) + config_dict = config_util.load_yaml_config_file(config_path) return from_config_dict(config_dict, hass, enable_log=False, skip_pip=skip_pip) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 10e18216ea0..0d82e1d2882 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -16,11 +16,11 @@ import itertools as it import logging import homeassistant.core as ha -import homeassistant.util as util -from homeassistant.helpers import extract_entity_ids +from homeassistant.helpers.entity import split_entity_id +from homeassistant.helpers.service import extract_entity_ids from homeassistant.loader import get_component from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ def is_on(hass, entity_id=None): entity_ids = hass.states.entity_ids() for entity_id in entity_ids: - domain = util.split_entity_id(entity_id)[0] + domain = split_entity_id(entity_id)[0] module = get_component(domain) @@ -68,6 +68,14 @@ def turn_off(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) +def toggle(hass, entity_id=None, **service_data): + """ Toggles specified entity. """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) + + def setup(hass, config): """ Setup general services related to homeassistant. """ @@ -84,7 +92,7 @@ def setup(hass, config): # Group entity_ids by domain. groupby requires sorted data. by_domain = it.groupby(sorted(entity_ids), - lambda item: util.split_entity_id(item)[0]) + lambda item: split_entity_id(item)[0]) for domain, ent_ids in by_domain: # We want to block for all calls and only return when all calls @@ -105,5 +113,6 @@ def setup(hass, config): hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) return True diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index 88967ec1f74..f7f6240b44e 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -9,11 +9,6 @@ https://home-assistant.io/components/arduino/ """ import logging -try: - from PyMata.pymata import PyMata -except ImportError: - PyMata = None - from homeassistant.helpers import validate_config from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) @@ -27,18 +22,12 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Setup the Arduino component. """ - global PyMata # pylint: disable=invalid-name - if PyMata is None: - from PyMata.pymata import PyMata as PyMata_ - PyMata = PyMata_ - - import serial - if not validate_config(config, {DOMAIN: ['port']}, _LOGGER): return False + import serial global BOARD try: BOARD = ArduinoBoard(config[DOMAIN]['port']) @@ -67,6 +56,7 @@ class ArduinoBoard(object): """ Represents an Arduino board. """ def __init__(self, port): + from PyMata.pymata import PyMata self._port = port self._board = PyMata(self._port, verbose=False) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 0616c0a48e6..6abb59eede6 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -10,7 +10,7 @@ import logging from datetime import timedelta from homeassistant.components import sun -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import track_sunrise, track_sunset import homeassistant.util.dt as dt_util DEPENDENCIES = ['sun'] @@ -47,9 +47,9 @@ def trigger(hass, config, action): # Do something to call action if event == EVENT_SUNRISE: - trigger_sunrise(hass, action, offset) + track_sunrise(hass, action, offset) else: - trigger_sunset(hass, action, offset) + track_sunset(hass, action, offset) return True @@ -125,44 +125,6 @@ def if_action(hass, config): return time_if -def trigger_sunrise(hass, action, offset): - """ Trigger action at next sun rise. """ - def next_rise(): - """ Returns next sunrise. """ - next_time = sun.next_rising_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - - def sunrise_automation_listener(now): - """ Called when it's time for action. """ - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) - action() - - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) - - -def trigger_sunset(hass, action, offset): - """ Trigger action at next sun set. """ - def next_set(): - """ Returns next sunrise. """ - next_time = sun.next_setting_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - - def sunset_automation_listener(now): - """ Called when it's time for action. """ - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) - action() - - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) - - def _parse_offset(raw_offset): if raw_offset is None: return timedelta(0) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index e8cf9c3b6ee..d02765f75c6 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -8,7 +8,6 @@ at https://home-assistant.io/components/automation/#time-trigger """ import logging -from homeassistant.util import convert import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_time_change @@ -34,9 +33,9 @@ def trigger(hass, config, action): hours, minutes, seconds = after.hour, after.minute, after.second elif (CONF_HOURS in config or CONF_MINUTES in config or CONF_SECONDS in config): - hours = convert(config.get(CONF_HOURS), int) - minutes = convert(config.get(CONF_MINUTES), int) - seconds = convert(config.get(CONF_SECONDS), int) + hours = config.get(CONF_HOURS) + minutes = config.get(CONF_MINUTES) + seconds = config.get(CONF_SECONDS) else: _LOGGER.error('One of %s, %s, %s OR %s needs to be specified', CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER) @@ -59,7 +58,7 @@ def if_action(hass, config): weekday = config.get(CONF_WEEKDAY) if before is None and after is None and weekday is None: - logging.getLogger(__name__).error( + _LOGGER.error( "Missing if-condition configuration key %s, %s or %s", CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY) return None diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py new file mode 100644 index 00000000000..8798e457e71 --- /dev/null +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -0,0 +1,81 @@ +""" +homeassistant.components.binary_sensor.command_sensor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure custom shell commands to turn a value +into a logical value for a binary sensor. +""" +import logging +from datetime import timedelta + +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.sensor.command_sensor import CommandSensorData +from homeassistant.util import template + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Binary Command Sensor" +DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_PAYLOAD_OFF = 'OFF' + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Add the Command Sensor. """ + + if config.get('command') is None: + _LOGGER.error('Missing required variable: "command"') + return False + + data = CommandSensorData(config.get('command')) + + add_devices([CommandBinarySensor( + hass, + data, + config.get('name', DEFAULT_NAME), + config.get('payload_on', DEFAULT_PAYLOAD_ON), + config.get('payload_off', DEFAULT_PAYLOAD_OFF), + config.get(CONF_VALUE_TEMPLATE) + )]) + + +# pylint: disable=too-many-arguments +class CommandBinarySensor(BinarySensorDevice): + """ Represents a binary sensor that is returning + a value of a shell commands. """ + def __init__(self, hass, data, name, payload_on, + payload_off, value_template): + self._hass = hass + self.data = data + self._name = name + self._state = False + self._payload_on = payload_on + self._payload_off = payload_off + self._value_template = value_template + self.update() + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def is_on(self): + """ True if the binary sensor is on. """ + return self._state + + def update(self): + """ Gets the latest data and updates the state. """ + self.data.update() + value = self.data.value + + if self._value_template is not None: + value = template.render_with_possible_json_value( + self._hass, self._value_template, value, False) + if value == self._payload_on: + self._state = True + elif value == self._payload_off: + self._state = False diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py new file mode 100644 index 00000000000..23925a1805b --- /dev/null +++ b/homeassistant/components/binary_sensor/nest.py @@ -0,0 +1,55 @@ +""" +homeassistant.components.binary_sensor.nest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Nest Thermostat Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.nest/ +""" +import logging +import socket +import homeassistant.components.nest as nest + +from homeassistant.components.sensor.nest import NestSensor +from homeassistant.components.binary_sensor import BinarySensorDevice + + +BINARY_TYPES = ['fan', + 'hvac_ac_state', + 'hvac_aux_heater_state', + 'hvac_heat_x2_state', + 'hvac_heat_x3_state', + 'hvac_alt_heat_state', + 'hvac_alt_heat_x2_state', + 'hvac_emer_heat_state', + 'online'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup Nest binary sensors. """ + + logger = logging.getLogger(__name__) + try: + for structure in nest.NEST.structures: + for device in structure.devices: + for variable in config['monitored_conditions']: + if variable in BINARY_TYPES: + add_devices([NestBinarySensor(structure, + device, + variable)]) + else: + logger.error('Nest sensor type: "%s" does not exist', + variable) + except socket.error: + logger.error( + "Connection error logging into the nest web service." + ) + + +class NestBinarySensor(NestSensor, BinarySensorDevice): + """ Represents a Nest binary sensor. """ + + @property + def is_on(self): + """ True if the binary sensor is on. """ + return bool(getattr(self.device, self.variable)) diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 2bb50fec766..64da1b5ea9f 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -1,7 +1,7 @@ """ homeassistant.components.binary_sensor.rpi_gpio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allows to configure a binary_sensor using RPi GPIO. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure a binary sensor using RPi GPIO. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rpi_gpio/ diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py new file mode 100644 index 00000000000..72b2499b190 --- /dev/null +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -0,0 +1,29 @@ +""" +homeassistant.components.binary_sensor.zigbee + +Contains functionality to use a ZigBee device as a binary sensor. +""" + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.zigbee import ( + ZigBeeDigitalIn, ZigBeeDigitalInConfig) + + +DEPENDENCIES = ["zigbee"] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ + Create and add an entity based on the configuration. + """ + add_entities([ + ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config)) + ]) + + +class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): + """ + Use multiple inheritance to turn a ZigBeeDigitalIn into a + BinarySensorDevice. + """ + pass diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 515daffc71c..6fb584635f9 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -11,7 +11,7 @@ the user has submitted configuration information. """ import logging -from homeassistant.helpers import generate_entity_id +from homeassistant.helpers.entity import generate_entity_id from homeassistant.const import EVENT_TIME_CHANGED DOMAIN = "configurator" diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 8b4b3fcce6c..37f93c0625d 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -62,10 +62,16 @@ def setup(hass, config): 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[2], lights[1], switches[0], - media_players[1]]) - group.setup_group(hass, 'bedroom', [lights[0], switches[1], - media_players[0]]) + group.Group(hass, 'living room', [ + lights[2], lights[1], switches[0], media_players[1], + 'scene.romantic_lights']) + group.Group(hass, 'bedroom', [lights[0], switches[1], + media_players[0]]) + group.Group(hass, 'Rooms', [ + 'group.living_room', 'group.bedroom', + 'scene.romantic_lights', 'rollershutter.kitchen_window', + 'rollershutter.living_room_window', + ], view=True) # Setup scripts bootstrap.setup_component( diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 204d845084c..c5b4ccd1c16 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -229,7 +229,7 @@ class DeviceTracker(object): """ 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.group = group.Group( self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) def update_stale(self, now): diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 73c2e98792f..9fee2747c0f 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -15,6 +15,8 @@ from homeassistant.helpers import validate_config from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN +REQUIREMENTS = ['fritzconnection==0.4.6'] + # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) @@ -55,16 +57,8 @@ class FritzBoxScanner(object): self.password = '' self.success_init = True - # Try to import the fritzconnection library - try: - # noinspection PyPackageRequirements,PyUnresolvedReferences - import fritzconnection as fc - except ImportError: - _LOGGER.exception("""Failed to import Python library - fritzconnection. Please run - /setup to install it.""") - self.success_init = False - return + # pylint: disable=import-error + import fritzconnection as fc # Check for user specific configuration if CONF_HOST in config.keys(): diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index ab1eccba769..233622e076e 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear==0.3.1'] +REQUIREMENTS = ['pynetgear==0.3.2'] def get_scanner(hass, config): diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index e1b0e1de306..0a924126b11 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -8,16 +8,26 @@ https://home-assistant.io/components/device_tracker.owntracks/ """ import json import logging +import threading +from collections import defaultdict import homeassistant.components.mqtt as mqtt -from homeassistant.const import (STATE_HOME, STATE_NOT_HOME) +from homeassistant.const import STATE_HOME DEPENDENCIES = ['mqtt'] -CONF_TRANSITION_EVENTS = 'use_events' +REGIONS_ENTERED = defaultdict(list) +MOBILE_BEACONS_ACTIVE = defaultdict(list) + +BEACON_DEV_ID = 'beacon' + LOCATION_TOPIC = 'owntracks/+/+' EVENT_TOPIC = 'owntracks/+/+/event' +_LOGGER = logging.getLogger(__name__) + +LOCK = threading.Lock() + def setup_scanner(hass, config, see): """ Set up an OwnTracks tracker. """ @@ -31,27 +41,28 @@ def setup_scanner(hass, config, see): data = json.loads(payload) except ValueError: # If invalid JSON - logging.getLogger(__name__).error( + _LOGGER.error( 'Unable to parse payload as JSON: %s', payload) return if not isinstance(data, dict) or data.get('_type') != 'location': return - parts = topic.split('/') - kwargs = { - 'dev_id': '{}_{}'.format(parts[1], parts[2]), - 'host_name': parts[1], - 'gps': (data['lat'], data['lon']), - } - if 'acc' in data: - kwargs['gps_accuracy'] = data['acc'] - if 'batt' in data: - kwargs['battery'] = data['batt'] + dev_id, kwargs = _parse_see_args(topic, data) - see(**kwargs) + # Block updates if we're in a region + with LOCK: + if REGIONS_ENTERED[dev_id]: + _LOGGER.debug( + "location update ignored - inside region %s", + REGIONS_ENTERED[-1]) + return + + see(**kwargs) + see_beacons(dev_id, kwargs) def owntracks_event_update(topic, payload, qos): + # pylint: disable=too-many-branches """ MQTT event (geofences) received. """ # Docs on available data: @@ -60,47 +71,111 @@ def setup_scanner(hass, config, see): data = json.loads(payload) except ValueError: # If invalid JSON - logging.getLogger(__name__).error( + _LOGGER.error( 'Unable to parse payload as JSON: %s', payload) return if not isinstance(data, dict) or data.get('_type') != 'transition': return - # check if in "home" fence or other zone - location = '' - if data['event'] == 'enter': + # OwnTracks uses - at the start of a beacon zone + # to switch on 'hold mode' - ignore this + location = data['desc'].lstrip("-") + if location.lower() == 'home': + location = STATE_HOME - if data['desc'].lower() == 'home': - location = STATE_HOME - else: - location = data['desc'] + dev_id, kwargs = _parse_see_args(topic, data) + + if data['event'] == 'enter': + zone = hass.states.get("zone.{}".format(location)) + with LOCK: + if zone is None: + if data['t'] == 'b': + # Not a HA zone, and a beacon so assume mobile + MOBILE_BEACONS_ACTIVE[dev_id].append(location) + else: + # Normal region + kwargs['location_name'] = location + + regions = REGIONS_ENTERED[dev_id] + if location not in regions: + regions.append(location) + _LOGGER.info("Enter region %s", location) + _set_gps_from_zone(kwargs, zone) + + see(**kwargs) + see_beacons(dev_id, kwargs) elif data['event'] == 'leave': - location = STATE_NOT_HOME + regions = REGIONS_ENTERED[dev_id] + if location in regions: + regions.remove(location) + new_region = regions[-1] if regions else None + + if new_region: + # Exit to previous region + zone = hass.states.get("zone.{}".format(new_region)) + kwargs['location_name'] = new_region + _set_gps_from_zone(kwargs, zone) + _LOGGER.info("Exit from to %s", new_region) + + else: + _LOGGER.info("Exit to GPS") + + see(**kwargs) + see_beacons(dev_id, kwargs) + + beacons = MOBILE_BEACONS_ACTIVE[dev_id] + if location in beacons: + beacons.remove(location) + else: - logging.getLogger(__name__).error( + _LOGGER.error( 'Misformatted mqtt msgs, _type=transition, event=%s', data['event']) return - parts = topic.split('/') - kwargs = { - 'dev_id': '{}_{}'.format(parts[1], parts[2]), - 'host_name': parts[1], - 'gps': (data['lat'], data['lon']), - 'location_name': location, - } - if 'acc' in data: - kwargs['gps_accuracy'] = data['acc'] + def see_beacons(dev_id, kwargs_param): + """ Set active beacons to the current location """ - see(**kwargs) + kwargs = kwargs_param.copy() + for beacon in MOBILE_BEACONS_ACTIVE[dev_id]: + kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) + kwargs['host_name'] = beacon + see(**kwargs) - use_events = config.get(CONF_TRANSITION_EVENTS) + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) - if use_events: - mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) - else: - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) return True + + +def _parse_see_args(topic, data): + """ Parse the OwnTracks location parameters, + into the format see expects. """ + + parts = topic.split('/') + dev_id = '{}_{}'.format(parts[1], parts[2]) + host_name = parts[1] + kwargs = { + 'dev_id': dev_id, + 'host_name': host_name, + 'gps': (data['lat'], data['lon']) + } + if 'acc' in data: + kwargs['gps_accuracy'] = data['acc'] + if 'batt' in data: + kwargs['battery'] = data['batt'] + return dev_id, kwargs + + +def _set_gps_from_zone(kwargs, zone): + """ Set the see parameters from the zone parameters """ + + if zone is not None: + kwargs['gps'] = ( + zone.attributes['latitude'], + zone.attributes['longitude']) + kwargs['gps_accuracy'] = zone.attributes['radius'] + return kwargs diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 655bf7d4eb6..3926495376c 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -11,6 +11,8 @@ import logging import re import threading +import requests + from homeassistant.helpers import validate_config from homeassistant.util import sanitize_filename @@ -30,14 +32,6 @@ def setup(hass, config): logger = logging.getLogger(__name__) - try: - import requests - except ImportError: - logger.exception(("Failed to import requests. " - "Did you maybe not execute 'pip install requests'?")) - - return False - if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger): return False diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 98f0435e9f3..06438c02140 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,9 @@ _LOGGER = logging.getLogger(__name__) FRONTEND_URLS = [ URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', - '/devEvent', '/devInfo', '/devTemplate', '/states'] + '/devEvent', '/devInfo', '/devTemplate', + re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'), +] _FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) diff --git a/homeassistant/components/frontend/mdi_version.py b/homeassistant/components/frontend/mdi_version.py index a8106ecd77e..4fa9a33b78c 100644 --- a/homeassistant/components/frontend/mdi_version.py +++ b/homeassistant/components/frontend/mdi_version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by update_mdi script """ -VERSION = "7d76081c37634d36af21f5cc1ca79408" +VERSION = "a2605736c8d959d50c4bcbba1e6a6aa5" diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index b8a31e418ca..ed2461d04a6 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 = "1003c31441ec44b3db84b49980f736a7" +VERSION = "1e89871aaae43c91b2508f52bc161b69" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 1816b922342..0a2643facff 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1437,7 +1437,7 @@ var n=this._rootDataHost;return n?n._scopeElementClass(t,e):void 0},stamp:functi left: 0; }; - }