diff --git a/.coveragerc b/.coveragerc index d078cd5bf8a..adb3c59765e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,8 @@ omit = homeassistant/__main__.py # omit pieces of code that rely on external devices being present + homeassistant/components/alarm_control_panel/alarmdotcom.py + homeassistant/components/arduino.py homeassistant/components/*/arduino.py @@ -15,6 +17,10 @@ omit = homeassistant/components/*/modbus.py homeassistant/components/*/tellstick.py + + homeassistant/components/tellduslive.py + homeassistant/components/*/tellduslive.py + homeassistant/components/*/vera.py homeassistant/components/ecobee.py @@ -32,6 +38,12 @@ omit = homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py + homeassistant/components/mysensors.py + homeassistant/components/*/mysensors.py + + homeassistant/components/rpi_gpio.py + homeassistant/components/*/rpi_gpio.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py @@ -41,7 +53,6 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py - homeassistant/components/device_tracker/geofancy.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py @@ -70,6 +81,7 @@ omit = homeassistant/components/media_player/plex.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/notify/free_mobile.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py @@ -89,10 +101,9 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py - homeassistant/components/sensor/mysensors.py + homeassistant/components/sensor/netatmo.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py - homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py @@ -108,13 +119,13 @@ omit = homeassistant/components/switch/mystrom.py homeassistant/components/switch/orvibo.py homeassistant/components/switch/rest.py - homeassistant/components/switch/rpi_gpio.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wemo.py 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 8935ffedc17..3ee71808ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ Icon dist build eggs +.eggs parts bin var diff --git a/.travis.yml b/.travis.yml index a75cf6685d3..c01b0750360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,7 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.5 - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi - - script/bootstrap_server + - "true" script: - script/cibuild matrix: diff --git a/LICENSE b/LICENSE index b3c5e1df750..42a425b4118 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Paulus Schoutsen +Copyright (c) 2016 Paulus Schoutsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7507fd12b8..b704fc082ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None): datefmt='%y-%m-%d %H:%M:%S')) logger = logging.getLogger('') logger.addHandler(err_handler) - logger.setLevel(logging.NOTSET) # this sets the minimum log level + logger.setLevel(logging.INFO) else: _LOGGER.error( diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index e0b008cab5e..10e18216ea0 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -87,13 +87,21 @@ def setup(hass, config): lambda item: util.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 + # have been processed. If a service does not exist it causes a 10 + # second delay while we're blocking waiting for a response. + # But services can be registered on other HA instances that are + # listening to the bus too. So as a in between solution, we'll + # block only if the service is defined in the current HA instance. + blocking = hass.services.has_service(domain, service.service) + # Create a new dict for this call data = dict(service.data) # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.services.call(domain, service.service, data, True) + hass.services.call(domain, service.service, data, blocking) hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py new file mode 100644 index 00000000000..7f76680feb7 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -0,0 +1,118 @@ +""" +homeassistant.components.alarm_control_panel.alarmdotcom +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Interfaces with Verisure alarm control panel. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ +""" +import logging + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +from homeassistant.const import ( + STATE_UNKNOWN, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) + +_LOGGER = logging.getLogger(__name__) + + +REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom' + '/archive/0.0.7.zip' + '#pyalarmdotcom==0.0.7'] +DEFAULT_NAME = 'Alarm.com' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup an Alarm.com control panel. """ + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + if username is None or password is None: + _LOGGER.error('Must specify username and password!') + return False + + add_devices([AlarmDotCom(hass, + config.get('name', DEFAULT_NAME), + config.get('code'), + username, + password)]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method +class AlarmDotCom(alarm.AlarmControlPanel): + """ Represents a Alarm.com status. """ + + def __init__(self, hass, name, code, username, password): + from pyalarmdotcom.pyalarmdotcom import Alarmdotcom + self._alarm = Alarmdotcom(username, password, timeout=10) + self._hass = hass + self._name = name + self._code = str(code) if code else None + self._username = username + self._password = password + + @property + def should_poll(self): + return True + + @property + def name(self): + return self._name + + @property + def code_format(self): + """ One or more characters if code is defined. """ + return None if self._code is None else '.+' + + @property + def state(self): + """ Returns the state of the device. """ + if self._alarm.state == 'Disarmed': + return STATE_ALARM_DISARMED + elif self._alarm.state == 'Armed Stay': + return STATE_ALARM_ARMED_HOME + elif self._alarm.state == 'Armed Away': + return STATE_ALARM_ARMED_AWAY + else: + return STATE_UNKNOWN + + def alarm_disarm(self, code=None): + """ Send disarm command. """ + if not self._validate_code(code, 'arming home'): + return + from pyalarmdotcom.pyalarmdotcom import Alarmdotcom + # Open another session to alarm.com to fire off the command + _alarm = Alarmdotcom(self._username, self._password, timeout=10) + _alarm.disarm() + self.update_ha_state() + + def alarm_arm_home(self, code=None): + """ Send arm home command. """ + if not self._validate_code(code, 'arming home'): + return + from pyalarmdotcom.pyalarmdotcom import Alarmdotcom + # Open another session to alarm.com to fire off the command + _alarm = Alarmdotcom(self._username, self._password, timeout=10) + _alarm.arm_stay() + self.update_ha_state() + + def alarm_arm_away(self, code=None): + """ Send arm away command. """ + if not self._validate_code(code, 'arming home'): + return + from pyalarmdotcom.pyalarmdotcom import Alarmdotcom + # Open another session to alarm.com to fire off the command + _alarm = Alarmdotcom(self._username, self._password, timeout=10) + _alarm.arm_away() + self.update_ha_state() + + def _validate_code(self, code, state): + """ Validate given code. """ + check = self._code is None or code == self._code + if not check: + _LOGGER.warning('Wrong code entered for %s', state) + return check diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 63bc989f3df..2658e005aea 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -68,7 +68,8 @@ class ManualAlarm(alarm.AlarmControlPanel): @property def state(self): """ Returns the state of the device. """ - if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \ + if self._state in (STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY) and \ self._pending_time and self._state_ts + self._pending_time > \ dt_util.utcnow(): return STATE_ALARM_PENDING diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index e4c498a5044..ecedd163d0b 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): alarms.extend([ VerisureAlarm(value) - for value in verisure.get_alarm_status().values() + for value in verisure.ALARM_STATUS.values() if verisure.SHOW_ALARM ]) @@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel): def __init__(self, alarm_status): self._id = alarm_status.id - self._device = verisure.MY_PAGES.DEVICE_ALARM self._state = STATE_UNKNOWN @property @@ -58,40 +57,40 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): """ Four digit code required. """ - return '^\\d{4}$' + return '^\\d{%s}$' % verisure.CODE_DIGITS def update(self): """ Update alarm status """ - verisure.update() + verisure.update_alarm() - if verisure.STATUS[self._device][self._id].status == 'unarmed': + if verisure.ALARM_STATUS[self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED - elif verisure.STATUS[self._device][self._id].status == 'armedhome': + elif verisure.ALARM_STATUS[self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME - elif verisure.STATUS[self._device][self._id].status == 'armedaway': + elif verisure.ALARM_STATUS[self._id].status == 'armed': self._state = STATE_ALARM_ARMED_AWAY - elif verisure.STATUS[self._device][self._id].status != 'pending': + elif verisure.ALARM_STATUS[self._id].status != 'pending': _LOGGER.error( 'Unknown alarm state %s', - verisure.STATUS[self._device][self._id].status) + verisure.ALARM_STATUS[self._id].status) def alarm_disarm(self, code=None): """ Send disarm command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_DISARMED) - _LOGGER.warning('disarming') + verisure.MY_PAGES.alarm.set(code, 'DISARMED') + _LOGGER.info('verisure alarm disarming') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() def alarm_arm_home(self, code=None): """ Send arm home command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_HOME) - _LOGGER.warning('arming home') + verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME') + _LOGGER.info('verisure alarm arming home') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() def alarm_arm_away(self, code=None): """ Send arm away command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_AWAY) - _LOGGER.warning('arming away') + verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY') + _LOGGER.info('verisure alarm arming away') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 0b06f3c9a79..66ac9de0b43 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -11,6 +11,7 @@ import logging from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.util import template +from homeassistant.helpers.service import call_from_config DOMAIN = 'alexa' DEPENDENCIES = ['http'] @@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa' CONF_INTENTS = 'intents' CONF_CARD = 'card' CONF_SPEECH = 'speech' +CONF_ACTION = 'action' def setup(hass, config): @@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data): speech = config.get(CONF_SPEECH) card = config.get(CONF_CARD) + action = config.get(CONF_ACTION) # pylint: disable=unsubscriptable-object if speech is not None: @@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data): response.add_card(CardType[card['type']], card['title'], card['content']) + if action is not None: + call_from_config(handler.server.hass, action, True) + handler.write_json(response.as_dict()) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 23d83f554ca..9c464f6954e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/ import logging from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.util import split_entity_id -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook +from homeassistant.helpers.service import call_from_config DOMAIN = 'automation' @@ -19,8 +19,6 @@ DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' CONF_SERVICE = 'service' -CONF_SERVICE_ENTITY_ID = 'entity_id' -CONF_SERVICE_DATA = 'data' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -96,22 +94,7 @@ def _get_action(hass, config, name): _LOGGER.info('Executing %s', name) logbook.log_entry(hass, name, 'has been triggered', DOMAIN) - domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = config.get(CONF_SERVICE_DATA, {}) - - if not isinstance(service_data, dict): - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) - service_data = {} - - if CONF_SERVICE_ENTITY_ID in config: - try: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID].split(",") - except AttributeError: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID] - - hass.services.call(domain, service, service_data) + call_from_config(hass, config) return action diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f2baf760748..61e68aa8e8e 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -6,6 +6,7 @@ Offers numeric state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#numeric-state-trigger """ +from functools import partial import logging from homeassistant.const import CONF_VALUE_TEMPLATE @@ -20,6 +21,14 @@ CONF_ABOVE = "above" _LOGGER = logging.getLogger(__name__) +def _renderer(hass, value_template, state): + """Render state value.""" + if value_template is None: + return state.state + + return template.render(hass, value_template, {'state': state}) + + def trigger(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) @@ -38,12 +47,7 @@ def trigger(hass, config, action): CONF_BELOW, CONF_ABOVE) return False - if value_template is not None: - renderer = lambda value: template.render(hass, - value_template, - {'state': value}) - else: - renderer = lambda value: value.state + renderer = partial(_renderer, hass, value_template) # pylint: disable=unused-argument def state_automation_listener(entity, from_s, to_s): @@ -79,12 +83,7 @@ def if_action(hass, config): CONF_BELOW, CONF_ABOVE) return None - if value_template is not None: - renderer = lambda value: template.render(hass, - value_template, - {'state': value}) - else: - renderer = lambda value: value.state + renderer = partial(_renderer, hass, value_template) def if_numeric_state(): """ Test numeric state condition. """ diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 84334493d0f..0616c0a48e6 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -17,6 +17,10 @@ DEPENDENCIES = ['sun'] CONF_OFFSET = 'offset' CONF_EVENT = 'event' +CONF_BEFORE = "before" +CONF_BEFORE_OFFSET = "before_offset" +CONF_AFTER = "after" +CONF_AFTER_OFFSET = "after_offset" EVENT_SUNSET = 'sunset' EVENT_SUNRISE = 'sunrise' @@ -37,26 +41,9 @@ def trigger(hass, config, action): _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) return False - if CONF_OFFSET in config: - raw_offset = config.get(CONF_OFFSET) - - negative_offset = False - if raw_offset.startswith('-'): - negative_offset = True - raw_offset = raw_offset[1:] - - try: - (hour, minute, second) = [int(x) for x in raw_offset.split(':')] - except ValueError: - _LOGGER.error('Could not parse offset %s', raw_offset) - return False - - offset = timedelta(hours=hour, minutes=minute, seconds=second) - - if negative_offset: - offset *= -1 - else: - offset = timedelta(0) + offset = _parse_offset(config.get(CONF_OFFSET)) + if offset is False: + return False # Do something to call action if event == EVENT_SUNRISE: @@ -67,6 +54,77 @@ def trigger(hass, config, action): return True +def if_action(hass, config): + """ Wraps action method with sun based condition. """ + before = config.get(CONF_BEFORE) + after = config.get(CONF_AFTER) + + # Make sure required configuration keys are present + if before is None and after is None: + logging.getLogger(__name__).error( + "Missing if-condition configuration key %s or %s", + CONF_BEFORE, CONF_AFTER) + return None + + # Make sure configuration keys have the right value + if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \ + after not in (None, EVENT_SUNRISE, EVENT_SUNSET): + logging.getLogger(__name__).error( + "%s and %s can only be set to %s or %s", + CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET) + return None + + before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET)) + after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET)) + if before_offset is False or after_offset is False: + return None + + if before is None: + def before_func(): + """Return no point in time.""" + return None + elif before == EVENT_SUNRISE: + def before_func(): + """Return time before sunrise.""" + return sun.next_rising(hass) + before_offset + else: + def before_func(): + """Return time before sunset.""" + return sun.next_setting(hass) + before_offset + + if after is None: + def after_func(): + """Return no point in time.""" + return None + elif after == EVENT_SUNRISE: + def after_func(): + """Return time after sunrise.""" + return sun.next_rising(hass) + after_offset + else: + def after_func(): + """Return time after sunset.""" + return sun.next_setting(hass) + after_offset + + def time_if(): + """ Validate time based if-condition """ + + now = dt_util.now() + before = before_func() + after = after_func() + + if before is not None and now > now.replace(hour=before.hour, + minute=before.minute): + return False + + if after is not None and now < now.replace(hour=after.hour, + minute=after.minute): + return False + + return True + + return time_if + + def trigger_sunrise(hass, action, offset): """ Trigger action at next sun rise. """ def next_rise(): @@ -103,3 +161,26 @@ def trigger_sunset(hass, action, offset): 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) + + negative_offset = False + if raw_offset.startswith('-'): + negative_offset = True + raw_offset = raw_offset[1:] + + try: + (hour, minute, second) = [int(x) for x in raw_offset.split(':')] + except ValueError: + _LOGGER.error('Could not parse offset %s', raw_offset) + return False + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if negative_offset: + offset *= -1 + + return offset diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 7fc2c0d40e2..e8cf9c3b6ee 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -32,8 +32,8 @@ def trigger(hass, config, action): _error_time(config[CONF_AFTER], CONF_AFTER) return False hours, minutes, seconds = after.hour, after.minute, after.second - elif (CONF_HOURS in config or CONF_MINUTES in config - or CONF_SECONDS in config): + 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) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 60963988f39..4d82d25e473 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rest/ """ -from datetime import timedelta import logging -import requests from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.util import template, Throttle +from homeassistant.util import template +from homeassistant.components.sensor.rest import RestData from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) @@ -19,61 +18,33 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_METHOD = 'GET' -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """ Get the REST binary sensor. """ - - use_get = False - use_post = False - + """Setup REST binary sensors.""" resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') - return False - except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', - resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestBinarySensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestBinarySensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """ Implements a REST binary sensor. """ + """REST binary sensor.""" def __init__(self, hass, rest, name, value_template): + """Initialize a REST binary sensor.""" self._hass = hass self.rest = rest self._name = name @@ -83,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the binary sensor. """ + """Name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ - if self.rest.data is False: + """Return if the binary sensor is on.""" + if self.rest.data is None: return False - else: - if self._value_template is not None: - self.rest.data = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) - return bool(int(self.rest.data)) + + if self._value_template is not None: + self.rest.data = template.render_with_possible_json_value( + self._hass, self._value_template, self.rest.data, False) + return bool(int(self.rest.data)) def update(self): - """ Gets the latest data from REST API and updates the state. """ + """Get the latest data from REST API and updates the state.""" self.rest.update() - - -# pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ - - def __init__(self, resource, verify_ssl): - self._resource = resource - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with GET method. """ - try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py new file mode 100644 index 00000000000..2bb50fec766 --- /dev/null +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -0,0 +1,73 @@ +""" +homeassistant.components.binary_sensor.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/ +""" + +import logging +import homeassistant.components.rpi_gpio as rpi_gpio +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import (DEVICE_DEFAULT_NAME) + +DEFAULT_PULL_MODE = "UP" +DEFAULT_BOUNCETIME = 50 +DEFAULT_INVERT_LOGIC = False + +DEPENDENCIES = ['rpi_gpio'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Raspberry PI GPIO devices. """ + + pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE) + bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME) + invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC) + + binary_sensors = [] + ports = config.get('ports') + for port_num, port_name in ports.items(): + binary_sensors.append(RPiGPIOBinarySensor( + port_name, port_num, pull_mode, bouncetime, invert_logic)) + add_devices(binary_sensors) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +class RPiGPIOBinarySensor(BinarySensorDevice): + """ Represents a binary sensor that uses Raspberry Pi GPIO. """ + def __init__(self, name, port, pull_mode, bouncetime, invert_logic): + # pylint: disable=no-member + + self._name = name or DEVICE_DEFAULT_NAME + self._port = port + self._pull_mode = pull_mode + self._bouncetime = bouncetime + self._invert_logic = invert_logic + + rpi_gpio.setup_input(self._port, self._pull_mode) + self._state = rpi_gpio.read_input(self._port) + + def read_gpio(port): + """ Reads state from GPIO. """ + self._state = rpi_gpio.read_input(self._port) + self.update_ha_state() + rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) + + @property + def should_poll(self): + """ No polling needed. """ + return False + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def is_on(self): + """ Returns the state of the entity. """ + return self._state != self._invert_logic diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index b90e1ee4448..472440d7307 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.username = str(config[CONF_USERNAME]) + self.password = str(config[CONF_PASSWORD]) self.lock = threading.Lock() diff --git a/homeassistant/components/device_tracker/geofancy.py b/homeassistant/components/device_tracker/geofancy.py deleted file mode 100644 index a5e6edee71a..00000000000 --- a/homeassistant/components/device_tracker/geofancy.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -homeassistant.components.device_tracker.geofancy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Geofancy platform for the device tracker. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.geofancy/ -""" -from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) - -DEPENDENCIES = ['http'] - -_SEE = 0 - -URL_API_GEOFANCY_ENDPOINT = "/api/geofancy" - - -def setup_scanner(hass, config, see): - """ Set up an endpoint for the Geofancy app. """ - - # Use a global variable to keep setup_scanner compact when using a callback - global _SEE - _SEE = see - - # POST would be semantically better, but that currently does not work - # since Geofancy sends the data as key1=value1&key2=value2 - # in the request body, while Home Assistant expects json there. - - hass.http.register_path( - 'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy) - - return True - - -def _handle_get_api_geofancy(handler, path_match, data): - """ Geofancy message received. """ - - if not isinstance(data, dict): - handler.write_json_message( - "Error while parsing Geofancy message.", - HTTP_INTERNAL_SERVER_ERROR) - return - if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message( - "Location not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - if 'device' not in data or 'id' not in data: - handler.write_json_message( - "Device id or location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - - try: - gps_coords = (float(data['latitude']), float(data['longitude'])) - except ValueError: - # If invalid latitude / longitude format - handler.write_json_message( - "Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) - return - - # entity id's in Home Assistant must be alphanumerical - device_uuid = data['device'] - device_entity_id = device_uuid.replace('-', '') - - _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) - - handler.write_json_message("Geofancy message processed") diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py new file mode 100644 index 00000000000..11884829600 --- /dev/null +++ b/homeassistant/components/device_tracker/locative.py @@ -0,0 +1,104 @@ +""" +homeassistant.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Locative platform for the device tracker. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.locative/ +""" +import logging +from functools import partial + +from homeassistant.const import ( + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) +from homeassistant.components.device_tracker import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['http'] + +URL_API_LOCATIVE_ENDPOINT = "/api/locative" + + +def setup_scanner(hass, config, see): + """ Set up an endpoint for the Locative app. """ + + # POST would be semantically better, but that currently does not work + # since Locative sends the data as key1=value1&key2=value2 + # in the request body, while Home Assistant expects json there. + + hass.http.register_path( + 'GET', URL_API_LOCATIVE_ENDPOINT, + partial(_handle_get_api_locative, hass, see)) + + return True + + +def _handle_get_api_locative(hass, see, handler, path_match, data): + """ Locative message received. """ + + if not _check_data(handler, data): + return + + device = data['device'].replace('-', '') + location_name = data['id'].lower() + direction = data['trigger'] + + if direction == 'enter': + see(dev_id=device, location_name=location_name) + handler.write_text("Setting location to {}".format(location_name)) + + elif direction == 'exit': + current_state = hass.states.get("{}.{}".format(DOMAIN, device)) + + if current_state is None or current_state.state == location_name: + see(dev_id=device, location_name=STATE_NOT_HOME) + handler.write_text("Setting location to not home") + else: + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered before + # the previous zone was exited. The enter message will be sent + # first, then the exit message will be sent second. + handler.write_text( + 'Ignoring exit from {} (already in {})'.format( + location_name, current_state)) + + elif direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + handler.write_text("Received test message.") + + else: + handler.write_text( + "Received unidentified message: {}".format(direction), + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Received unidentified message from Locative: %s", + direction) + + +def _check_data(handler, data): + if 'latitude' not in data or 'longitude' not in data: + handler.write_text("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Latitude and longitude not specified.") + return False + + if 'device' not in data: + handler.write_text("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Device id not specified.") + return False + + if 'id' not in data: + handler.write_text("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Location id not specified.") + return False + + if 'trigger' not in data: + handler.write_text("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Trigger is not specified.") + return False + + return True diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 5d20e98e992..ab1eccba769 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'] +REQUIREMENTS = ['pynetgear==0.3.1'] def get_scanner(hass, config): diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index b98c3a1636c..e1b0e1de306 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -10,14 +10,17 @@ import json import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import (STATE_HOME, STATE_NOT_HOME) DEPENDENCIES = ['mqtt'] +CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' +EVENT_TOPIC = 'owntracks/+/+/event' def setup_scanner(hass, config, see): - """ Set up a OwnTracksks tracker. """ + """ Set up an OwnTracks tracker. """ def owntracks_location_update(topic, payload, qos): """ MQTT message received. """ @@ -48,6 +51,56 @@ def setup_scanner(hass, config, see): see(**kwargs) - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + def owntracks_event_update(topic, payload, qos): + """ MQTT event (geofences) received. """ + + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typetransition + try: + data = json.loads(payload) + except ValueError: + # If invalid JSON + logging.getLogger(__name__).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': + + if data['desc'].lower() == 'home': + location = STATE_HOME + else: + location = data['desc'] + + elif data['event'] == 'leave': + location = STATE_NOT_HOME + else: + logging.getLogger(__name__).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'] + + see(**kwargs) + + use_events = config.get(CONF_TRANSITION_EVENTS) + + if use_events: + mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + else: + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) return True diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index e69de29bb2d..dc573ae0275 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -0,0 +1,33 @@ +# Describes the format for available device tracker services + +see: + description: Control tracked device + + fields: + mac: + description: MAC address of device + example: 'FF:FF:FF:FF:FF:FF' + + dev_id: + description: Id of device (find id in known_devices.yaml) + example: 'phonedave' + + host_name: + description: Hostname of device + example: 'Dave' + + location_name: + description: Name of location where device is located (not_home is away) + example: 'home' + + gps: + description: GPS coordinates where device is located (latitude, longitude) + example: '[51.509802, -0.086692]' + + gps_accuracy: + description: Accuracy of GPS coordinates + example: '80' + + battery: + description: Battery level of device + example: '100' diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 868f701673a..cd0e8239c38 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -105,8 +105,7 @@ class SnmpScanner(object): return if errstatus: _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), - errindex and restable[-1][int(errindex)-1] - or '?') + errindex and restable[-1][int(errindex)-1] or '?') return for resrow in restable: diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 46556b3eca4..a661dac0c1e 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -242,8 +242,8 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): _LOGGER.info("Loading wireless clients...") - url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \ - .format(self.host, self.stok) + url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?' + 'form=statistics').format(self.host, self.stok) referer = 'http://{}/webpages/index.html'.format(self.host) response = requests.post(url, diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 64845a350ca..b8a31e418ca 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 = "be08c5a3ce12040bbdba2db83cb1a568" +VERSION = "1003c31441ec44b3db84b49980f736a7" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 8df0a4724a0..1816b922342 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,4 +1,4 @@ -
Call a service from a component.
Call a service from a component.
Fire an event on the event bus.
Fire an event on the event bus.
[[stateObj.attributes.description]]
[[stateObj.attributes.errors]]
[[stateObj.attributes.description]]
[[stateObj.attributes.errors]]