diff --git a/.coveragerc b/.coveragerc index 2364cb4d8d1..28b4b926eab 100644 --- a/.coveragerc +++ b/.coveragerc @@ -37,6 +37,7 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py + homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/thomson.py @@ -49,8 +50,10 @@ omit = homeassistant/components/light/hue.py homeassistant/components/light/limitlessled.py homeassistant/components/light/blinksticklight.py + homeassistant/components/light/hyperion.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py + homeassistant/components/media_player/firetv.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py @@ -70,6 +73,7 @@ omit = homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/command_sensor.py + homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/dht.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/forecast.py @@ -89,10 +93,12 @@ omit = homeassistant/components/switch/command_switch.py homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.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/nest.py + homeassistant/components/thermostat/radiotherm.py [report] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f646766a231..106b914eecb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,8 @@ After you finish adding support for your device: - Update the supported devices in the `README.md` file. - Add any new dependencies to `requirements_all.txt`. There is no ordering right now, so just add it to the end. - Update the `.coveragerc` file. - - Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io). - - Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`. + - Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io). It's OK to add a docstring with configuration details to the file header. + - Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `./script/lint`. - Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant. - Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/). diff --git a/Dockerfile b/Dockerfile index 9554ec552d7..9344ec65245 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,10 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends nmap net-tools && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Open Z-Wave disabled because broken -#RUN apt-get update && \ -# apt-get install -y cython3 libudev-dev && \ -# apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ -# pip3 install cython && \ -# scripts/build_python_openzwave +RUN apt-get update && \ + apt-get install -y cython3 libudev-dev && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ + pip3 install "cython<0.23" && \ + script/build_python_openzwave CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/README.md b/README.md index 36777acc517..2fed012402c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Examples of devices it can interface it: * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT * * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)) + * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)), and Amazon Fire TV (by way of [python-firetv](https://github.com/happyleavesaoc/python-firetv)) * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) * Interaction with [IFTTT](https://ifttt.com/) * Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org). diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index b85305b6d18..d3289e08e62 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -8,7 +8,7 @@ import os from homeassistant.components import verisure from homeassistant.const import ( - ATTR_ENTITY_ID, + ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import Entity @@ -29,6 +29,7 @@ SERVICE_TO_METHOD = { SERVICE_ALARM_DISARM: 'alarm_disarm', SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', + SERVICE_ALARM_TRIGGER: 'alarm_trigger' } ATTR_CODE = 'code' @@ -53,9 +54,9 @@ def setup(hass, config): target_alarms = component.extract_from_service(service) if ATTR_CODE not in service.data: - return - - code = service.data[ATTR_CODE] + code = None + else: + code = service.data[ATTR_CODE] method = SERVICE_TO_METHOD[service.service] @@ -72,36 +73,50 @@ def setup(hass, config): return True -def alarm_disarm(hass, code, entity_id=None): +def alarm_disarm(hass, code=None, entity_id=None): """ Send the alarm the command for disarm. """ - data = {ATTR_CODE: code} - + data = {} + if code: + data[ATTR_CODE] = code if entity_id: data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) -def alarm_arm_home(hass, code, entity_id=None): +def alarm_arm_home(hass, code=None, entity_id=None): """ Send the alarm the command for arm home. """ - data = {ATTR_CODE: code} - + data = {} + if code: + data[ATTR_CODE] = code if entity_id: data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) -def alarm_arm_away(hass, code, entity_id=None): +def alarm_arm_away(hass, code=None, entity_id=None): """ Send the alarm the command for arm away. """ - data = {ATTR_CODE: code} - + data = {} + if code: + data[ATTR_CODE] = code if entity_id: data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) +def alarm_trigger(hass, code=None, entity_id=None): + """ Send the alarm the command for trigger. """ + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) + + # pylint: disable=no-self-use class AlarmControlPanel(Entity): """ ABC for alarm control devices. """ @@ -123,6 +138,10 @@ class AlarmControlPanel(Entity): """ Send arm away command. """ raise NotImplementedError() + def alarm_trigger(self, code=None): + """ Send alarm trigger command. """ + raise NotImplementedError() + @property def state_attributes(self): """ Return the state attributes. """ diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py new file mode 100644 index 00000000000..8c98eec50e1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -0,0 +1,149 @@ +""" +homeassistant.components.alarm_control_panel.manual +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for manual alarms. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.manual.html +""" +import logging +import datetime +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.helpers.event import track_point_in_time +import homeassistant.util.dt as dt_util + +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = [] + +DEFAULT_ALARM_NAME = 'HA Alarm' +DEFAULT_PENDING_TIME = 60 +DEFAULT_TRIGGER_TIME = 120 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the manual alarm platform. """ + + add_devices([ManualAlarm( + hass, + config.get('name', DEFAULT_ALARM_NAME), + config.get('code'), + config.get('pending_time', DEFAULT_PENDING_TIME), + config.get('trigger_time', DEFAULT_TRIGGER_TIME), + )]) + + +# pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method +class ManualAlarm(alarm.AlarmControlPanel): + """ + Represents an alarm status. + + When armed, will be pending for 'pending_time', after that armed. + When triggered, will be pending for 'trigger_time'. After that will be + triggered for 'trigger_time', after that we return to disarmed. + """ + + def __init__(self, hass, name, code, pending_time, trigger_time): + self._state = STATE_ALARM_DISARMED + self._hass = hass + self._name = name + self._code = str(code) if code else None + self._pending_time = datetime.timedelta(seconds=pending_time) + self._trigger_time = datetime.timedelta(seconds=trigger_time) + self._state_ts = None + + @property + def should_poll(self): + """ No polling needed. """ + return False + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if self._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 + + if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state_ts + self._pending_time > dt_util.utcnow(): + return STATE_ALARM_PENDING + elif (self._state_ts + self._pending_time + + self._trigger_time) < dt_util.utcnow(): + return STATE_ALARM_DISARMED + + return self._state + + @property + def code_format(self): + """ One or more characters. """ + return None if self._code is None else '.+' + + def alarm_disarm(self, code=None): + """ Send disarm command. """ + if not self._validate_code(code, STATE_ALARM_DISARMED): + return + + self._state = STATE_ALARM_DISARMED + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + def alarm_arm_home(self, code=None): + """ Send arm home command. """ + if not self._validate_code(code, STATE_ALARM_ARMED_HOME): + return + + self._state = STATE_ALARM_ARMED_HOME + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._pending_time) + + def alarm_arm_away(self, code=None): + """ Send arm away command. """ + if not self._validate_code(code, STATE_ALARM_ARMED_AWAY): + return + + self._state = STATE_ALARM_ARMED_AWAY + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._pending_time) + + def alarm_trigger(self, code=None): + """ Send alarm trigger command. No code needed. """ + self._state = STATE_ALARM_TRIGGERED + self._state_ts = dt_util.utcnow() + self.update_ha_state() + + if self._trigger_time: + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._pending_time) + + track_point_in_time( + self._hass, self.update_ha_state, + self._state_ts + self._pending_time + self._trigger_time) + + def _validate_code(self, code, state): + """ Validate given code. """ + check = self._code is None or code == self._code + if not check: + _LOGGER.warning('Invalid code given for %s', state) + return check diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index c04c8ee6031..e070babd080 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -1,68 +1,18 @@ """ homeassistant.components.alarm_control_panel.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This platform enables the possibility to control a MQTT alarm. -In this platform, 'state_topic' and 'command_topic' are required. -The alarm will only change state after receiving the a new state -from 'state_topic'. If these messages are published with RETAIN flag, -the MQTT alarm will receive an instant state update after subscription -and will start with correct state. Otherwise, the initial state will -be 'unknown'. -Configuration: - -alarm_control_panel: - platform: mqtt - name: "MQTT Alarm" - state_topic: "home/alarm" - command_topic: "home/alarm/set" - qos: 0 - payload_disarm: "DISARM" - payload_arm_home: "ARM_HOME" - payload_arm_away: "ARM_AWAY" - code: "mySecretCode" - -Variables: - -name -*Optional -The name of the alarm. Default is 'MQTT Alarm'. - -state_topic -*Required -The MQTT topic subscribed to receive state updates. - -command_topic -*Required -The MQTT topic to publish commands to change the alarm state. - -qos -*Optional -The maximum QoS level of the state topic. Default is 0. -This QoS will also be used to publishing messages. - -payload_disarm -*Optional -The payload do disarm alarm. Default is "DISARM". - -payload_arm_home -*Optional -The payload to set armed-home mode. Default is "ARM_HOME". - -payload_arm_away -*Optional -The payload to set armed-away mode. Default is "ARM_AWAY". - -code -*Optional -If defined, specifies a code to enable or disable the alarm in the frontend. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.mqtt.html """ import logging import homeassistant.components.mqtt as mqtt import homeassistant.components.alarm_control_panel as alarm -from homeassistant.const import (STATE_UNKNOWN) +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) @@ -99,6 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method class MqttAlarm(alarm.AlarmControlPanel): """ represents a MQTT alarm status within home assistant. """ @@ -113,10 +64,15 @@ class MqttAlarm(alarm.AlarmControlPanel): self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away - self._code = code + self._code = str(code) if code else None def message_received(topic, payload, qos): """ A new MQTT message has been received. """ + if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED): + _LOGGER.warning('Received unexpected payload: %s', payload) + return self._state = payload self.update_ha_state() @@ -144,24 +100,28 @@ class MqttAlarm(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """ Send disarm command. """ - if code == str(self._code) or self.code_format is None: - mqtt.publish(self.hass, self._command_topic, - self._payload_disarm, self._qos) - else: - _LOGGER.warning("Wrong code entered while disarming!") + if not self._validate_code(code, 'disarming'): + return + mqtt.publish(self.hass, self._command_topic, + self._payload_disarm, self._qos) def alarm_arm_home(self, code=None): """ Send arm home command. """ - if code == str(self._code) or self.code_format is None: - mqtt.publish(self.hass, self._command_topic, - self._payload_arm_home, self._qos) - else: - _LOGGER.warning("Wrong code entered while arming home!") + if not self._validate_code(code, 'arming home'): + return + mqtt.publish(self.hass, self._command_topic, + self._payload_arm_home, self._qos) def alarm_arm_away(self, code=None): """ Send arm away command. """ - if code == str(self._code) or self.code_format is None: - mqtt.publish(self.hass, self._command_topic, - self._payload_arm_away, self._qos) - else: - _LOGGER.warning("Wrong code entered while arming away!") + if not self._validate_code(code, 'arming away'): + return + mqtt.publish(self.hass, self._command_topic, + self._payload_arm_away, self._qos) + + 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/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index c7c24a60c4a..9e0475592bd 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -33,6 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(alarms) +# pylint: disable=abstract-method class VerisureAlarm(alarm.AlarmControlPanel): """ Represents a Verisure alarm status. """ diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index e4c794df424..b11525170a4 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -1,8 +1,10 @@ """ homeassistant.components.api ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides a Rest API for Home Assistant. + +For more details about the RESTful API, please refer to the documentation at +https://home-assistant.io/developers/api.html """ import re import logging diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index cbb319e2541..12ceafbd44b 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -4,26 +4,8 @@ components.arduino Arduino component that connects to a directly attached Arduino board which runs with the Firmata firmware. -Configuration: - -To use the Arduino board you will need to add something like the following -to your configuration.yaml file. - -arduino: - port: /dev/ttyACM0 - -Variables: - -port -*Required -The port where is your board connected to your Home Assistant system. -If you are using an original Arduino the port will be named ttyACM*. The exact -number can be determined with 'ls /dev/ttyACM*' or check your 'dmesg'/ -'journalctl -f' output. Keep in mind that Arduino clones are often using a -different name for the port (e.g. '/dev/ttyUSB*'). - -A word of caution: The Arduino is not storing states. This means that with -every initialization the pins are set to off/low. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/arduino.html """ import logging diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index c5b0ee47923..c172b8e0e11 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.event ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers event listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#event-trigger """ import logging diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 3f85792f907..706d97824b4 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers MQTT listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#mqtt-trigger """ import logging diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 7e014213d62..1ddfb91a334 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.numeric_state ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 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.html#numeric-state-trigger """ import logging diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 8baa0a01d46..52379355d6b 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.state ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers state listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#state-trigger """ import logging @@ -28,6 +30,11 @@ def trigger(hass, config, action): from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL + if isinstance(from_state, bool) or isinstance(to_state, bool): + logging.getLogger(__name__).error( + 'Config error. Surround to/from values with quotes.') + return False + def state_automation_listener(entity, from_s, to_s): """ Listens for state changes and calls action. """ action() diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 103df6c9b39..c72474ae4dd 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.sun ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers sun based automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#sun-trigger """ import logging from datetime import timedelta diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 1d97ccc135d..2f05c6f4390 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers time listening automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#time-trigger """ import logging diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index f62aec8bf2a..28d1c8456f0 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,8 +1,10 @@ """ homeassistant.components.automation.zone ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Offers zone automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/components/automation.html#zone-trigger """ import logging diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 78fd0f4d2e1..24a42cbf883 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -3,42 +3,6 @@ homeassistant.components.camera.foscam ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This component provides basic support for Foscam IP cameras. -As part of the basic support the following features will be provided: --MJPEG video streaming - -To use this component, add the following to your configuration.yaml file. - -camera: - platform: foscam - name: Door Camera - ip: 192.168.0.123 - port: 88 - username: YOUR_USERNAME - password: YOUR_PASSWORD - -Variables: - -ip -*Required -The IP address of your Foscam device. - -username -*Required -The username of a visitor or operator of your camera. Oddly admin accounts -don't seem to have access to take snapshots. - -password -*Required -The password for accessing your camera. - -name -*Optional -This parameter allows you to override the name of your camera in homeassistant. - -port -*Optional -The port that the camera is running on. The default is 88. - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.foscam.html """ diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 7e4a24ffdfe..74d2d0102d3 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -3,43 +3,8 @@ homeassistant.components.camera.generic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for IP Cameras. -This component provides basic support for IP cameras. For the basic support to -work you camera must support accessing a JPEG snapshot via a URL and you will -need to specify the "still_image_url" parameter which should be the location of -the JPEG image. - -As part of the basic support the following features will be provided: -- MJPEG video streaming -- Saving a snapshot -- Recording(JPEG frame capture) - -To use this component, add the following to your configuration.yaml file. - -camera: - platform: generic - name: Door Camera - username: YOUR_USERNAME - password: YOUR_PASSWORD - still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg - -Variables: - -still_image_url -*Required -The URL your camera serves the image on, eg. http://192.168.1.21:2112/ - -name -*Optional -This parameter allows you to override the name of your camera in Home -Assistant. - -username -*Optional -The username for accessing your camera. - -password -*Optional -The password for accessing your camera. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.generic.html """ import logging from requests.auth import HTTPBasicAuth diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index fd2ad60d211..f00a640232f 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -1,9 +1,10 @@ """ homeassistant.components.conversation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to have conversations with Home Assistant. -This is more a proof of concept. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/conversation.html """ import logging import re diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index f22135ec5bc..388a869ae0c 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -63,6 +63,14 @@ def setup(hass, config): 'still_image_url': 'http://home-assistant.io/demo/webcam.jpg', }}) + # Setup alarm_control_panel + bootstrap.setup_component( + hass, 'alarm_control_panel', + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'Test Alarm', + }}) + # Setup scripts bootstrap.setup_component( hass, 'script', diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index f926b182983..a2f94f34c3a 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -4,41 +4,8 @@ homeassistant.components.device_tracker.actiontec Device tracker platform that supports scanning an Actiontec MI424WR (Verizon FIOS) router for device presence. -This device tracker needs telnet to be enabled on the router. - -Configuration: - -To use the Actiontec tracker you will need to add something like the -following to your configuration.yaml file. If you experience disconnects -you can modify the home_interval variable. - -device_tracker: - platform: actiontec - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - # optional: - home_interval: 10 - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. - -home_interval -*Optional -If the home_interval is set then the component will not let a device -be AWAY if it has been HOME in the last home_interval minutes. This is -in addition to the 3 minute wait built into the device_tracker component. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.actiontec.html """ import logging from datetime import timedelta @@ -56,7 +23,7 @@ from homeassistant.components.device_tracker import DOMAIN # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) -# interval in minutes to exclude devices from a scan while they are home +# Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = "home_interval" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index 68ff8390216..d46264fa264 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -4,33 +4,8 @@ homeassistant.components.device_tracker.aruba Device tracker platform that supports scanning a Aruba Access Point for device presence. -This device tracker needs telnet to be enabled on the router. - -Configuration: - -To use the Aruba tracker you will need to add something like the following -to your configuration.yaml file. You also need to enable Telnet in the -configuration page of your router. - -device_tracker: - platform: aruba - host: YOUR_ACCESS_POINT_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.aruba.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 1e3ac20b6f2..5284d45835b 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -161,9 +161,10 @@ class AsusWrtDeviceScanner(object): # For leases where the client doesn't set a hostname, ensure # it is blank and not '*', which breaks the entity_id down # the line - host = match.group('host') - if host == '*': - host = '' + if match: + host = match.group('host') + if host == '*': + host = '' devices[match.group('ip')] = { 'host': host, @@ -174,6 +175,6 @@ class AsusWrtDeviceScanner(object): for neighbor in neighbors: match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) - if match.group('ip') in devices: + if match and match.group('ip') in devices: devices[match.group('ip')]['status'] = match.group('status') return devices diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 947876c85b5..d8734a55a17 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -4,30 +4,8 @@ homeassistant.components.device_tracker.ddwrt Device tracker platform that supports scanning a DD-WRT router for device presence. -Configuration: - -To use the DD-WRT tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: ddwrt - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ddwrt.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/geofancy.py b/homeassistant/components/device_tracker/geofancy.py new file mode 100644 index 00000000000..91d3978326b --- /dev/null +++ b/homeassistant/components/device_tracker/geofancy.py @@ -0,0 +1,71 @@ +""" +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.html +""" + +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/luci.py b/homeassistant/components/device_tracker/luci.py index 4cbc6a2d492..2ce032f90fd 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -4,33 +4,8 @@ homeassistant.components.device_tracker.luci Device tracker platform that supports scanning a OpenWRT router for device presence. -It's required that the luci RPC package is installed on the OpenWRT router: -# opkg install luci-mod-rpc - -Configuration: - -To use the Luci tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: luci - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.luci.html """ import logging import json diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 34cee8f6733..f78cb3420f5 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -1,15 +1,10 @@ """ homeassistant.components.device_tracker.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - MQTT platform for the device tracker. -device_tracker: - platform: mqtt - qos: 1 - devices: - paulus_oneplus: /location/paulus - annetherese_n4: /location/annetherese +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.mqtt.html """ import logging from homeassistant import util diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 46c515dcb1f..2d138cf5c70 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -4,30 +4,8 @@ homeassistant.components.device_tracker.netgear Device tracker platform that supports scanning a Netgear router for device presence. -Configuration: - -To use the Netgear tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: netgear - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.netgear.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 6f993f0fc7e..fe6b814b96f 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -3,26 +3,8 @@ homeassistant.components.device_tracker.nmap ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Device tracker platform that supports scanning a network with nmap. -Configuration: - -To use the nmap tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: nmap_tracker - hosts: 192.168.1.1/24 - -Variables: - -hosts -*Required -The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or -the range notation (192.168.1.1-255). - -home_interval -*Optional -Number of minutes it will not scan devices that it found in previous results. -This is to save battery. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.nmap_scanner.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 505fd6b7ad2..78fd42f1566 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,11 +1,10 @@ """ homeassistant.components.device_tracker.owntracks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - OwnTracks platform for the device tracker. -device_tracker: - platform: owntracks +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.owntracks.html """ import json import logging diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py index 408daa94d81..c6679e6c320 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -4,32 +4,8 @@ homeassistant.components.device_tracker.thomson Device tracker platform that supports scanning a THOMSON router for device presence. -This device tracker needs telnet to be enabled on the router. - -Configuration: - -To use the THOMSON tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: thomson - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.thomson.html """ import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index a23b7b80ff0..df0c9c8d93d 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -4,36 +4,8 @@ homeassistant.components.device_tracker.tomato Device tracker platform that supports scanning a Tomato router for device presence. -Configuration: - -To use the Tomato tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: tomato - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - http_id: ABCDEFG - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. - -http_id -*Required -The value can be obtained by logging in to the Tomato admin interface and -search for http_id in the page source code. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.tomato.html """ import logging import json diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 6b12000cf45..3769229f101 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -4,30 +4,8 @@ homeassistant.components.device_tracker.tplink Device tracker platform that supports scanning a TP-Link router for device presence. -Configuration: - -To use the TP-Link tracker you will need to add something like the following -to your configuration.yaml file. - -device_tracker: - platform: tplink - host: YOUR_ROUTER_IP - username: YOUR_ADMIN_USERNAME - password: YOUR_ADMIN_PASSWORD - -Variables: - -host -*Required -The IP address of your router, e.g. 192.168.1.1. - -username -*Required -The username of an user with administrative privileges, usually 'admin'. - -password -*Required -The password for your given admin account. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.tplink.html """ import base64 import logging @@ -54,10 +32,13 @@ def get_scanner(hass, config): _LOGGER): return None - scanner = Tplink2DeviceScanner(config[DOMAIN]) + scanner = Tplink3DeviceScanner(config[DOMAIN]) if not scanner.success_init: - scanner = TplinkDeviceScanner(config[DOMAIN]) + scanner = Tplink2DeviceScanner(config[DOMAIN]) + + if not scanner.success_init: + scanner = TplinkDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -156,7 +137,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): with self.lock: _LOGGER.info("Loading wireless clients...") - url = 'http://{}/data/map_access_wireless_client_grid.json'\ + url = 'http://{}/data/map_access_wireless_client_grid.json' \ .format(self.host) referer = 'http://{}'.format(self.host) @@ -166,7 +147,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): b64_encoded_username_password = base64.b64encode( username_password.encode('ascii') ).decode('ascii') - cookie = 'Authorization=Basic {}'\ + cookie = 'Authorization=Basic {}' \ .format(b64_encoded_username_password) response = requests.post(url, headers={'referer': referer, @@ -183,7 +164,119 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): self.last_results = { device['mac_addr'].replace('-', ':'): device['name'] for device in result - } + } + return True + + return False + + +class Tplink3DeviceScanner(TplinkDeviceScanner): + """ + This class queries the Archer C9 router running version 150811 or higher + of TP-Link firmware for connected devices. + """ + + def __init__(self, config): + self.stok = '' + self.sysauth = '' + super(Tplink3DeviceScanner, self).__init__(config) + + def scan_devices(self): + """ + Scans for new devices and return a list containing found device ids. + """ + + self._update_info() + return self.last_results.keys() + + # pylint: disable=no-self-use + def get_device_name(self, device): + """ + The TP-Link firmware doesn't save the name of the wireless device. + We are forced to use the MAC address as name here. + """ + + return self.last_results.get(device) + + def _get_auth_tokens(self): + """ + Retrieves auth tokens from the router. + """ + + _LOGGER.info("Retrieving auth tokens...") + + url = 'http://{}/cgi-bin/luci/;stok=/login?form=login' \ + .format(self.host) + referer = 'http://{}/webpages/login.html'.format(self.host) + + # if possible implement rsa encryption of password here + + response = requests.post(url, + params={'operation': 'login', + 'username': self.username, + 'password': self.password}, + headers={'referer': referer}) + + try: + self.stok = response.json().get('data').get('stok') + _LOGGER.info(self.stok) + regex_result = re.search('sysauth=(.*);', + response.headers['set-cookie']) + self.sysauth = regex_result.group(1) + _LOGGER.info(self.sysauth) + return True + except ValueError: + _LOGGER.error("Couldn't fetch auth tokens!") + return False + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensures the information from the TP-Link router is up to date. + Returns boolean if scanning successful. + """ + + with self.lock: + if (self.stok == '') or (self.sysauth == ''): + self._get_auth_tokens() + + _LOGGER.info("Loading wireless clients...") + + 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, + params={'operation': 'load'}, + headers={'referer': referer}, + cookies={'sysauth': self.sysauth}) + + try: + json_response = response.json() + + if json_response.get('success'): + result = response.json().get('data') + else: + if json_response.get('errorcode') == 'timeout': + _LOGGER.info("Token timed out. " + "Relogging on next scan.") + self.stok = '' + self.sysauth = '' + return False + else: + _LOGGER.error("An unknown error happened " + "while fetching data.") + return False + except ValueError: + _LOGGER.error("Router didn't respond with JSON. " + "Check if credentials are correct.") + return False + + if result: + self.last_results = { + device['mac'].replace('-', ':'): device['mac'] + for device in result + } return True return False diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py new file mode 100644 index 00000000000..195ed33e77b --- /dev/null +++ b/homeassistant/components/device_tracker/ubus.py @@ -0,0 +1,173 @@ +""" +homeassistant.components.device_tracker.ubus +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a OpenWRT router for device +presence. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ubus.html +""" +import logging +import json +from datetime import timedelta +import re +import threading +import requests + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + + +def get_scanner(hass, config): + """ Validates config and returns a Luci scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = UbusDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +# pylint: disable=too-many-instance-attributes +class UbusDeviceScanner(object): + """ + This class queries a wireless router running OpenWrt firmware + for connected devices. Adapted from Tomato scanner. + + Configure your routers' ubus ACL based on following instructions: + + http://wiki.openwrt.org/doc/techref/ubus + + Read only access will be fine. + + To use this class you have to install rpcd-mod-file package + in your OpenWrt router: + + opkg install rpcd-mod-file + + """ + + def __init__(self, config): + host = config[CONF_HOST] + username, password = config[CONF_USERNAME], config[CONF_PASSWORD] + + self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") + self.lock = threading.Lock() + self.last_results = {} + self.url = 'http://{}/ubus'.format(host) + + self.session_id = _get_session_id(self.url, username, password) + self.hostapd = [] + self.leasefile = None + self.mac2name = None + self.success_init = self.session_id is not None + + def scan_devices(self): + """ + Scans for new devices and return a list containing found device ids. + """ + + self._update_info() + + return self.last_results + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + + with self.lock: + if self.leasefile is None: + result = _req_json_rpc(self.url, self.session_id, + 'call', 'uci', 'get', + config="dhcp", type="dnsmasq") + if result: + values = result["values"].values() + self.leasefile = next(iter(values))["leasefile"] + else: + return + + if self.mac2name is None: + result = _req_json_rpc(self.url, self.session_id, + 'call', 'file', 'read', + path=self.leasefile) + if result: + self.mac2name = dict() + for line in result["data"].splitlines(): + hosts = line.split(" ") + self.mac2name[hosts[1].upper()] = hosts[3] + else: + # Error, handled in the _req_json_rpc + return + + return self.mac2name.get(device.upper(), None) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensures the information from the Luci router is up to date. + Returns boolean if scanning successful. + """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + + if not self.hostapd: + hostapd = _req_json_rpc(self.url, self.session_id, + 'list', 'hostapd.*', '') + self.hostapd.extend(hostapd.keys()) + + self.last_results = [] + results = 0 + for hostapd in self.hostapd: + result = _req_json_rpc(self.url, self.session_id, + 'call', hostapd, 'get_clients') + + if result: + results = results + 1 + self.last_results.extend(result['clients'].keys()) + + return bool(results) + + +def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): + """ Perform one JSON RPC operation. """ + + data = json.dumps({"jsonrpc": "2.0", + "id": 1, + "method": rpcmethod, + "params": [session_id, + subsystem, + method, + params]}) + + try: + res = requests.post(url, data=data, timeout=5) + + except requests.exceptions.Timeout: + return + + if res.status_code == 200: + response = res.json() + + if rpcmethod == "call": + return response["result"][1] + else: + return response["result"] + + +def _get_session_id(url, username, password): + """ Get authentication token for the given host+username+password. """ + res = _req_json_rpc(url, "00000000000000000000000000000000", 'call', + 'session', 'login', username=username, + password=password) + return res["ubus_rpc_session"] diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 089db3fb324..f61d792bc60 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -19,7 +19,7 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['netdisco==0.4.2'] +REQUIREMENTS = ['netdisco==0.5.1'] SCAN_INTERVAL = 300 # seconds @@ -28,6 +28,7 @@ SERVICE_HUE = 'philips_hue' SERVICE_CAST = 'google_cast' SERVICE_NETGEAR = 'netgear_router' SERVICE_SONOS = 'sonos' +SERVICE_PLEX = 'plex_mediaserver' SERVICE_HANDLERS = { SERVICE_WEMO: "switch", @@ -35,6 +36,7 @@ SERVICE_HANDLERS = { SERVICE_HUE: "light", SERVICE_NETGEAR: 'device_tracker', SERVICE_SONOS: 'media_player', + SERVICE_PLEX: 'media_player', } @@ -88,6 +90,7 @@ def setup(hass, config): ATTR_DISCOVERED: info }) + # pylint: disable=unused-argument def start_discovery(event): """ Start discovering. """ netdisco = DiscoveryService(SCAN_INTERVAL) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 6978dbd7fa9..f145fadfb71 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -1,8 +1,10 @@ """ homeassistant.components.downloader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to download files. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/downloader.html """ import os import logging @@ -42,6 +44,10 @@ def setup(hass, config): download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] + # If path is relative, we assume relative to HASS config dir + if not os.path.isabs(download_path): + download_path = hass.config.path(download_path) + if not os.path.isdir(download_path): logger.error( diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index abf0c498b1a..1c753d1638e 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 = "c4722afa376379bc4457d54bb9a38cee" +VERSION = "beb922c55bb26ea576581b453f6d7c04" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 73fdb905114..7343bd3afd0 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,584 @@ - \ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 6989009b2d5..24623ff26ab 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 6989009b2d59e39fd39b3025ff5899877f618bd3 +Subproject commit 24623ff26ab8cbf7b39f0a25c26d9d991063b61a diff --git a/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png b/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png new file mode 100644 index 00000000000..97a1b4b352c Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png differ diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js index ec6063b7a58..faa2558e457 100644 --- a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js +++ b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js @@ -7,6 +7,6 @@ * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ -// @version 0.7.12 -window.WebComponents=window.WebComponents||{},function(e){var t=e.flags||{},n="webcomponents-lite.js",r=document.querySelector('script[src*="'+n+'"]');if(!t.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var n,r=e.split("=");r[0]&&(n=r[0].match(/wc-(.+)/))&&(t[n[1]]=r[1]||!0)}),r)for(var o,i=0;o=r.attributes[i];i++)"src"!==o.name&&(t[o.name]=o.value||!0);if(t.log){var a=t.log.split(",");t.log={},a.forEach(function(e){t.log[e]=!0})}else t.log={}}t.shadow=t.shadow||t.shadowdom||t.polyfill,t.shadow="native"===t.shadow?!1:t.shadow||!HTMLElement.prototype.createShadowRoot,t.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=t.register),e.flags=t}(window.WebComponents),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",u=0,l="",_=!1,w=!1,g=[];e:for(;(e[u-1]!=f||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(f==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):f!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[u+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),f==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||f!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){_&&(c("@ already seen."),l+="%40"),_=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=p(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;np&&(h=s[p]);p++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),p=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},f=p(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return p(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(f,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),_=v?"complete":"interactive",w="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=f.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),f.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=f,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=i.getResponseHeader("Location"),a=null;if(n)var a="/"===n.substr(0,1)?location.origin+n:n;r.call(o,!t.ok(i)&&i,i.response||i.responseText,a)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.dispatchEvent(e.__resource&&!e.__error?new CustomEvent("load",{bubbles:!1}):new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return t(e)&&void 0===e.__doc?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){p.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);p.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},p=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var f={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",f),Object.defineProperty(c,"baseURI",f)}e.importer=h,e.importLoader=p}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(o)}var n=e.initializeModules,r=e.isIE;if(!e.useNative){r&&"function"!=typeof window.CustomEvent&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),n();var o=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function o(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,r=t.length;r>n&&(e=t[n]);n++)e();L=[]}function a(e){y?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),g(e,function(e){d(e)})}function d(e){y?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){w.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function p(e,n){if(w.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(M(e.addedNodes,function(e){e.localName&&t(e,a)}),M(e.removedNodes,function(e){e.localName&&c(e)}))}),w.dom&&console.groupEnd()}function f(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(p(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(p.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),w.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),w.dom&&console.groupEnd()}function _(e){b(e,v)}var w=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=!window.MutationObserver||window.MutationObserver===window.JsMutationObserver;e.hasPolyfillMutations=y;var E=!1,L=[],M=Array.prototype.forEach.call.bind(Array.prototype.forEach),T=Element.prototype.createShadowRoot;T&&(Element.prototype.createShadowRoot=function(){var e=T.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=_,e.upgradeDocument=v,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=f}),window.CustomElements.addModule(function(e){function t(t,r){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&_(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){ -var t=e.setAttribute;e.setAttribute=function(e,n){r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function f(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return w(e),e}}var m,v=e.isIE,_=e.upgradeDocumentTree,w=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},M="http://www.w3.org/1999/xhtml",T=document.createElement.bind(document),N=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},f(Node.prototype,"cloneNode"),f(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=p,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){a(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules,o=e.isIE;if(n){var i=function(){};e.watchShadow=i,e.upgrade=i,e.upgradeAll=i,e.upgradeDocumentTree=i,e.upgradeSubtree=i,e.takeRecords=i,e["instanceof"]=function(e,t){return e instanceof t}}else r();var a=e.upgradeDocumentTree,s=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&s(wrap(e["import"]))}),o&&"function"!=typeof window.CustomEvent&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var c=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(c,t)}else t()}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){function e(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case" ":return" "}}function t(t){return t.replace(a,e)}var n="template",r=document.implementation.createHTMLDocument("template"),o=!0;HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=r.createDocumentFragment());for(var n;n=e.firstChild;)e.content.appendChild(n);if(o)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(r.body.innerHTML=e,HTMLTemplateElement.bootstrap(r);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;r.body.firstChild;)this.content.appendChild(r.body.firstChild)},configurable:!0})}catch(i){o=!1}},HTMLTemplateElement.bootstrap=function(e){for(var t,r=e.querySelectorAll(n),o=0,i=r.length;i>o&&(t=r[o]);o++)HTMLTemplateElement.decorate(t)},window.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var i=document.createElement;document.createElement=function(){"use strict";var e=i.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e};var a=/[&\u00A0<>]/g}(),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file +// @version 0.7.15 +!function(){window.WebComponents=window.WebComponents||{flags:{}};var e="webcomponents-lite.js",t=document.querySelector('script[src*="'+e+'"]'),n={};if(!n.noOpts){if(location.search.slice(1).split("&").forEach(function(e){var t,r=e.split("=");r[0]&&(t=r[0].match(/wc-(.+)/))&&(n[t[1]]=r[1]||!0)}),t)for(var r,o=0;r=t.attributes[o];o++)"src"!==r.name&&(n[r.name]=r.value||!0);if(n.log&&n.log.split){var i=n.log.split(",");n.log={},i.forEach(function(e){n.log[e]=!0})}else n.log={}}n.register&&(window.CustomElements=window.CustomElements||{flags:{}},window.CustomElements.flags.register=n.register),WebComponents.flags=n}(),function(e){"use strict";function t(e){return void 0!==h[e]}function n(){s.call(this),this._isInvalid=!0}function r(e){return""==e&&n.call(this),e.toLowerCase()}function o(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,63,96].indexOf(t)?e:encodeURIComponent(e)}function i(e){var t=e.charCodeAt(0);return t>32&&127>t&&-1==[34,35,60,62,96].indexOf(t)?e:encodeURIComponent(e)}function a(e,a,s){function c(e){g.push(e)}var d=a||"scheme start",u=0,l="",w=!1,_=!1,g=[];e:for(;(e[u-1]!=p||0==u)&&!this._isInvalid;){var b=e[u];switch(d){case"scheme start":if(!b||!m.test(b)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=b.toLowerCase(),d="scheme";break;case"scheme":if(b&&v.test(b))l+=b.toLowerCase();else{if(":"!=b){if(a){if(p==b)break e;c("Code point not allowed in scheme: "+b);break e}l="",u=0,d="no scheme";continue}if(this._scheme=l,l="",a)break e;t(this._scheme)&&(this._isRelative=!0),d="file"==this._scheme?"relative":this._isRelative&&s&&s._scheme==this._scheme?"relative or authority":this._isRelative?"authority first slash":"scheme data"}break;case"scheme data":"?"==b?(this._query="?",d="query"):"#"==b?(this._fragment="#",d="fragment"):p!=b&&" "!=b&&"\n"!=b&&"\r"!=b&&(this._schemeData+=o(b));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=b||"/"!=e[u+1]){c("Expected /, got: "+b),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),p==b){this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._username=s._username,this._password=s._password;break e}if("/"==b||"\\"==b)"\\"==b&&c("\\ is an invalid code point."),d="relative slash";else if("?"==b)this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query="?",this._username=s._username,this._password=s._password,d="query";else{if("#"!=b){var y=e[u+1],E=e[u+2];("file"!=this._scheme||!m.test(b)||":"!=y&&"|"!=y||p!=E&&"/"!=E&&"\\"!=E&&"?"!=E&&"#"!=E)&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password,this._path=s._path.slice(),this._path.pop()),d="relative path";continue}this._host=s._host,this._port=s._port,this._path=s._path.slice(),this._query=s._query,this._fragment="#",this._username=s._username,this._password=s._password,d="fragment"}break;case"relative slash":if("/"!=b&&"\\"!=b){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==b&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=b){c("Expected '/', got: "+b),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=b){c("Expected '/', got: "+b);continue}break;case"authority ignore slashes":if("/"!=b&&"\\"!=b){d="authority";continue}c("Expected authority, got: "+b);break;case"authority":if("@"==b){w&&(c("@ already seen."),l+="%40"),w=!0;for(var L=0;L>>0)+(t++ +"__")};n.prototype={set:function(t,n){var r=t[this.name];return r&&r[0]===t?r[1]=n:e(t,this.name,{value:[t,n],writable:!0}),this},get:function(e){var t;return(t=e[this.name])&&t[0]===e?t[1]:void 0},"delete":function(e){var t=e[this.name];return t&&t[0]===e?(t[0]=t[1]=void 0,!0):!1},has:function(e){var t=e[this.name];return t?t[0]===e:!1}},window.WeakMap=n}(),function(e){function t(e){b.push(e),g||(g=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){g=!1;var e=b;b=[],e.sort(function(e,t){return e.uid_-t.uid_});var t=!1;e.forEach(function(e){var n=e.takeRecords();o(e),n.length&&(e.callback_(n,e),t=!0)}),t&&r()}function o(e){e.nodes_.forEach(function(t){var n=v.get(t);n&&n.forEach(function(t){t.observer===e&&t.removeTransientObservers()})})}function i(e,t){for(var n=e;n;n=n.parentNode){var r=v.get(n);if(r)for(var o=0;o0){var o=n[r-1],i=f(o,e);if(i)return void(n[r-1]=i)}else t(this.observer);n[r]=e},addListeners:function(){this.addListeners_(this.target)},addListeners_:function(e){var t=this.options;t.attributes&&e.addEventListener("DOMAttrModified",this,!0),t.characterData&&e.addEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.addEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.addEventListener("DOMNodeRemoved",this,!0)},removeListeners:function(){this.removeListeners_(this.target)},removeListeners_:function(e){var t=this.options;t.attributes&&e.removeEventListener("DOMAttrModified",this,!0),t.characterData&&e.removeEventListener("DOMCharacterDataModified",this,!0),t.childList&&e.removeEventListener("DOMNodeInserted",this,!0),(t.childList||t.subtree)&&e.removeEventListener("DOMNodeRemoved",this,!0)},addTransientObserver:function(e){if(e!==this.target){this.addListeners_(e),this.transientObservedNodes.push(e);var t=v.get(e);t||v.set(e,t=[]),t.push(this)}},removeTransientObservers:function(){var e=this.transientObservedNodes;this.transientObservedNodes=[],e.forEach(function(e){this.removeListeners_(e);for(var t=v.get(e),n=0;nf&&(h=s[f]);f++)a(h)?(c++,n()):(h.addEventListener("load",r),h.addEventListener("error",i));else n()}function a(e){return l?e.__loaded||e["import"]&&"loading"!==e["import"].readyState:e.__importParsed}function s(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)c(t)&&d(t)}function c(e){return"link"===e.localName&&"import"===e.rel}function d(e){var t=e["import"];t?o({target:e}):(e.addEventListener("load",o),e.addEventListener("error",o))}var u="import",l=Boolean(u in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),f=function(e){return h?window.ShadowDOMPolyfill.wrapIfNeeded(e):e},p=f(document),m={get:function(){var e=window.HTMLImports.currentScript||document.currentScript||("complete"!==document.readyState?document.scripts[document.scripts.length-1]:null);return f(e)},configurable:!0};Object.defineProperty(document,"_currentScript",m),Object.defineProperty(p,"_currentScript",m);var v=/Trident/.test(navigator.userAgent),w=v?"complete":"interactive",_="readystatechange";l&&(new MutationObserver(function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.addedNodes&&s(t.addedNodes)}).observe(document.head,{childList:!0}),function(){if("loading"===document.readyState)for(var e,t=document.querySelectorAll("link[rel=import]"),n=0,r=t.length;r>n&&(e=t[n]);n++)d(e)}()),t(function(e){window.HTMLImports.ready=!0,window.HTMLImports.readyTime=(new Date).getTime();var t=p.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),p.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=u,e.useNative=l,e.rootDocument=p,e.whenReady=t,e.isIE=v}(window.HTMLImports),function(e){var t=[],n=function(e){t.push(e)},r=function(){t.forEach(function(t){t(e)})};e.addModule=n,e.initializeModules=r}(window.HTMLImports),window.HTMLImports.addModule(function(e){var t=/(url\()([^)]*)(\))/g,n=/(@import[\s]+(?!url\())([^;]*)(;)/g,r={resolveUrlsInStyle:function(e,t){var n=e.ownerDocument,r=n.createElement("a");return e.textContent=this.resolveUrlsInCssText(e.textContent,t,r),e},resolveUrlsInCssText:function(e,r,o){var i=this.replaceUrls(e,o,r,t);return i=this.replaceUrls(i,o,r,n)},replaceUrls:function(e,t,n,r){return e.replace(r,function(e,r,o,i){var a=o.replace(/["']/g,"");return n&&(a=new URL(a,n).href),t.href=a,a=t.href,r+"'"+a+"'"+i})}};e.path=r}),window.HTMLImports.addModule(function(e){var t={async:!0,ok:function(e){return e.status>=200&&e.status<300||304===e.status||0===e.status},load:function(n,r,o){var i=new XMLHttpRequest;return(e.flags.debug||e.flags.bust)&&(n+="?"+Math.random()),i.open("GET",n,t.async),i.addEventListener("readystatechange",function(e){if(4===i.readyState){var n=i.getResponseHeader("Location"),a=null;if(n)var a="/"===n.substr(0,1)?location.origin+n:n;r.call(o,!t.ok(i)&&i,i.response||i.responseText,a)}}),i.send(),i},loadDocument:function(e,t,n){this.load(e,t,n).responseType="document"}};e.xhr=t}),window.HTMLImports.addModule(function(e){var t=e.xhr,n=e.flags,r=function(e,t){this.cache={},this.onload=e,this.oncomplete=t,this.inflight=0,this.pending={}};r.prototype={addNodes:function(e){this.inflight+=e.length;for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)this.require(t);this.checkDone()},addNode:function(e){this.inflight++,this.require(e),this.checkDone()},require:function(e){var t=e.src||e.href;e.__nodeUrl=t,this.dedupe(t,e)||this.fetch(t,e)},dedupe:function(e,t){if(this.pending[e])return this.pending[e].push(t),!0;return this.cache[e]?(this.onload(e,t,this.cache[e]),this.tail(),!0):(this.pending[e]=[t],!1)},fetch:function(e,r){if(n.load&&console.log("fetch",e,r),e)if(e.match(/^data:/)){var o=e.split(","),i=o[0],a=o[1];a=i.indexOf(";base64")>-1?atob(a):decodeURIComponent(a),setTimeout(function(){this.receive(e,r,null,a)}.bind(this),0)}else{var s=function(t,n,o){this.receive(e,r,t,n,o)}.bind(this);t.load(e,s)}else setTimeout(function(){this.receive(e,r,{error:"href must be specified"},null)}.bind(this),0)},receive:function(e,t,n,r,o){this.cache[e]=r;for(var i,a=this.pending[e],s=0,c=a.length;c>s&&(i=a[s]);s++)this.onload(e,i,r,n,o),this.tail();this.pending[e]=null},tail:function(){--this.inflight,this.checkDone()},checkDone:function(){this.inflight||this.oncomplete()}},e.Loader=r}),window.HTMLImports.addModule(function(e){var t=function(e){this.addCallback=e,this.mo=new MutationObserver(this.handler.bind(this))};t.prototype={handler:function(e){for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)"childList"===t.type&&t.addedNodes.length&&this.addedNodes(t.addedNodes)},addedNodes:function(e){this.addCallback&&this.addCallback(e);for(var t,n=0,r=e.length;r>n&&(t=e[n]);n++)t.children&&t.children.length&&this.addedNodes(t.children)},observe:function(e){this.mo.observe(e,{childList:!0,subtree:!0})}},e.Observer=t}),window.HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===u}function n(e){var t=r(e);return"data:text/javascript;charset=utf-8,"+encodeURIComponent(t)}function r(e){return e.textContent+o(e)}function o(e){var t=e.ownerDocument;t.__importedScripts=t.__importedScripts||0;var n=e.ownerDocument.baseURI,r=t.__importedScripts?"-"+t.__importedScripts:"";return t.__importedScripts++,"\n//# sourceURL="+n+r+".js\n"}function i(e){var t=e.ownerDocument.createElement("style");return t.textContent=e.textContent,a.resolveUrlsInStyle(t),t}var a=e.path,s=e.rootDocument,c=e.flags,d=e.isIE,u=e.IMPORT_LINK_TYPE,l="link[rel="+u+"]",h={documentSelectors:l,importsSelectors:[l,"link[rel=stylesheet]:not([type])","style:not([type])","script:not([type])",'script[type="application/javascript"]','script[type="text/javascript"]'].join(","),map:{link:"parseLink",script:"parseScript",style:"parseStyle"},dynamicElements:[],parseNext:function(){var e=this.nextToParse();e&&this.parse(e)},parse:function(e){if(this.isParsed(e))return void(c.parse&&console.log("[%s] is already parsed",e.localName));var t=this[this.map[e.localName]];t&&(this.markParsing(e),t.call(this,e))},parseDynamic:function(e,t){this.dynamicElements.push(e),t||this.parseNext()},markParsing:function(e){c.parse&&console.log("parsing",e),this.parsingElement=e},markParsingComplete:function(e){e.__importParsed=!0,this.markDynamicParsingComplete(e),e.__importElement&&(e.__importElement.__importParsed=!0,this.markDynamicParsingComplete(e.__importElement)),this.parsingElement=null,c.parse&&console.log("completed",e)},markDynamicParsingComplete:function(e){var t=this.dynamicElements.indexOf(e);t>=0&&this.dynamicElements.splice(t,1)},parseImport:function(e){if(e["import"]=e.__doc,window.HTMLImports.__importsParsingHook&&window.HTMLImports.__importsParsingHook(e),e["import"]&&(e["import"].__importParsed=!0),this.markParsingComplete(e),e.__resource&&!e.__error?e.dispatchEvent(new CustomEvent("load",{bubbles:!1})):e.dispatchEvent(new CustomEvent("error",{bubbles:!1})),e.__pending)for(var t;e.__pending.length;)t=e.__pending.shift(),t&&t({target:e});this.parseNext()},parseLink:function(e){t(e)?this.parseImport(e):(e.href=e.href,this.parseGeneric(e))},parseStyle:function(e){var t=e;e=i(e),t.__appliedElement=e,e.__importElement=t,this.parseGeneric(e)},parseGeneric:function(e){this.trackElement(e),this.addElementToDocument(e)},rootImportForElement:function(e){for(var t=e;t.ownerDocument.__importLink;)t=t.ownerDocument.__importLink;return t},addElementToDocument:function(e){var t=this.rootImportForElement(e.__importElement||e);t.parentNode.insertBefore(e,t)},trackElement:function(e,t){var n=this,r=function(o){e.removeEventListener("load",r),e.removeEventListener("error",r),t&&t(o),n.markParsingComplete(e),n.parseNext()};if(e.addEventListener("load",r),e.addEventListener("error",r),d&&"style"===e.localName){var o=!1;if(-1==e.textContent.indexOf("@import"))o=!0;else if(e.sheet){o=!0;for(var i,a=e.sheet.cssRules,s=a?a.length:0,c=0;s>c&&(i=a[c]);c++)i.type===CSSRule.IMPORT_RULE&&(o=o&&Boolean(i.styleSheet))}o&&setTimeout(function(){e.dispatchEvent(new CustomEvent("load",{bubbles:!1}))})}},parseScript:function(t){var r=document.createElement("script");r.__importElement=t,r.src=t.src?t.src:n(t),e.currentScript=t,this.trackElement(r,function(t){r.parentNode&&r.parentNode.removeChild(r),e.currentScript=null}),this.addElementToDocument(r)},nextToParse:function(){return this._mayParse=[],!this.parsingElement&&(this.nextToParseInDoc(s)||this.nextToParseDynamic())},nextToParseInDoc:function(e,n){if(e&&this._mayParse.indexOf(e)<0){this._mayParse.push(e);for(var r,o=e.querySelectorAll(this.parseSelectorsForNode(e)),i=0,a=o.length;a>i&&(r=o[i]);i++)if(!this.isParsed(r))return this.hasResource(r)?t(r)?this.nextToParseInDoc(r.__doc,r):r:void 0}return n},nextToParseDynamic:function(){return this.dynamicElements[0]},parseSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===s?this.documentSelectors:this.importsSelectors},isParsed:function(e){return e.__importParsed},needsDynamicParsing:function(e){return this.dynamicElements.indexOf(e)>=0},hasResource:function(e){return t(e)&&void 0===e.__doc?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=l}),window.HTMLImports.addModule(function(e){function t(e){return n(e,a)}function n(e,t){return"link"===e.localName&&e.getAttribute("rel")===t}function r(e){return!!Object.getOwnPropertyDescriptor(e,"baseURI")}function o(e,t){var n=document.implementation.createHTMLDocument(a);n._URL=t;var o=n.createElement("base");o.setAttribute("href",t),n.baseURI||r(n)||Object.defineProperty(n,"baseURI",{value:t});var i=n.createElement("meta");return i.setAttribute("charset","utf-8"),n.head.appendChild(i),n.head.appendChild(o),n.body.innerHTML=e,window.HTMLTemplateElement&&HTMLTemplateElement.bootstrap&&HTMLTemplateElement.bootstrap(n),n}var i=e.flags,a=e.IMPORT_LINK_TYPE,s=e.IMPORT_SELECTOR,c=e.rootDocument,d=e.Loader,u=e.Observer,l=e.parser,h={documents:{},documentPreloadSelectors:s,importsPreloadSelectors:[s].join(","),loadNode:function(e){f.addNode(e)},loadSubtree:function(e){var t=this.marshalNodes(e);f.addNodes(t)},marshalNodes:function(e){return e.querySelectorAll(this.loadSelectorsForNode(e))},loadSelectorsForNode:function(e){var t=e.ownerDocument||e;return t===c?this.documentPreloadSelectors:this.importsPreloadSelectors},loaded:function(e,n,r,a,s){if(i.load&&console.log("loaded",e,n),n.__resource=r,n.__error=a,t(n)){var c=this.documents[e];void 0===c&&(c=a?null:o(r,s||e),c&&(c.__importLink=n,this.bootDocument(c)),this.documents[e]=c),n.__doc=c}l.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),l.parseNext()},loadedAll:function(){l.parseNext()}},f=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new u,!document.baseURI){var p={get:function(){var e=document.querySelector("base");return e?e.href:window.location.href},configurable:!0};Object.defineProperty(document,"baseURI",p),Object.defineProperty(c,"baseURI",p)}e.importer=h,e.importLoader=f}),window.HTMLImports.addModule(function(e){var t=e.parser,n=e.importer,r={added:function(e){for(var r,o,i,a,s=0,c=e.length;c>s&&(a=e[s]);s++)r||(r=a.ownerDocument,o=t.isParsed(r)),i=this.shouldLoadNode(a),i&&n.loadNode(a),this.shouldParseNode(a)&&o&&t.parseDynamic(a,i)},shouldLoadNode:function(e){return 1===e.nodeType&&o.call(e,n.loadSelectorsForNode(e))},shouldParseNode:function(e){return 1===e.nodeType&&o.call(e,t.parseSelectorsForNode(e))}};n.observer.addCallback=r.added.bind(r);var o=HTMLElement.prototype.matches||HTMLElement.prototype.matchesSelector||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector}),function(e){function t(){window.HTMLImports.importer.bootDocument(o)}var n=e.initializeModules,r=e.isIE;if(!e.useNative){(!window.CustomEvent||r&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),n();var o=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(window.HTMLImports),window.CustomElements=window.CustomElements||{flags:{}},function(e){var t=e.flags,n=[],r=function(e){n.push(e)},o=function(){n.forEach(function(t){t(e)})};e.addModule=r,e.initializeModules=o,e.hasNative=Boolean(document.registerElement),e.isIE=/Trident/.test(navigator.userAgent),e.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||window.HTMLImports.useNative)}(window.CustomElements),window.CustomElements.addModule(function(e){function t(e,t){n(e,function(e){return t(e)?!0:void r(e,t)}),r(e,t)}function n(e,t,r){var o=e.firstElementChild;if(!o)for(o=e.firstChild;o&&o.nodeType!==Node.ELEMENT_NODE;)o=o.nextSibling;for(;o;)t(o,r)!==!0&&n(o,t,r),o=o.nextElementSibling;return null}function r(e,n){for(var r=e.shadowRoot;r;)t(r,n),r=r.olderShadowRoot}function o(e,t){i(e,t,[])}function i(e,t,n){if(e=window.wrap(e),!(n.indexOf(e)>=0)){n.push(e);for(var r,o=e.querySelectorAll("link[rel="+a+"]"),s=0,c=o.length;c>s&&(r=o[s]);s++)r["import"]&&i(r["import"],t,n);t(e)}}var a=window.HTMLImports?window.HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),window.CustomElements.addModule(function(e){function t(e,t){return n(e,t)||r(e,t)}function n(t,n){return e.upgrade(t,n)?!0:void(n&&a(t))}function r(e,t){g(e,function(e){return n(e,t)?!0:void 0})}function o(e){L.push(e),E||(E=!0,setTimeout(i))}function i(){E=!1;for(var e,t=L,n=0,r=t.length;r>n&&(e=t[n]);n++)e();L=[]}function a(e){y?o(function(){s(e)}):s(e)}function s(e){e.__upgraded__&&!e.__attached&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function c(e){d(e),g(e,function(e){d(e)})}function d(e){y?o(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&e.__attached&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function l(e){for(var t=e,n=window.wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function h(e){if(e.shadowRoot&&!e.shadowRoot.__watched){_.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)m(t),t=t.olderShadowRoot}}function f(e,n){if(_.dom){var r=n[0];if(r&&"childList"===r.type&&r.addedNodes&&r.addedNodes){for(var o=r.addedNodes[0];o&&o!==document&&!o.host;)o=o.parentNode;var i=o&&(o.URL||o._URL||o.host&&o.host.localName)||"";i=i.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",n.length,i||"")}var a=l(e);n.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e,a)}),T(e.removedNodes,function(e){e.localName&&c(e)}))}),_.dom&&console.groupEnd()}function p(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(e,t.takeRecords()),i())}function m(e){if(!e.__observer){var t=new MutationObserver(f.bind(this,e));t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function v(e){e=window.wrap(e),_.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop());var n=e===window.wrap(document);t(e,n),m(e),_.dom&&console.groupEnd()}function w(e){b(e,v)}var _=e.flags,g=e.forSubtree,b=e.forDocumentTree,y=window.MutationObserver._isPolyfilled&&_["throttle-attached"];e.hasPolyfillMutations=y,e.hasThrottledAttached=y;var E=!1,L=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),M=Element.prototype.createShadowRoot;M&&(Element.prototype.createShadowRoot=function(){var e=M.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=h,e.upgradeDocumentTree=w,e.upgradeDocument=v,e.upgradeSubtree=r,e.upgradeAll=t,e.attached=a,e.takeRecords=p}),window.CustomElements.addModule(function(e){function t(t,r){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var o=t.getAttribute("is"),i=e.getRegisteredDefinition(t.localName)||e.getRegisteredDefinition(o);if(i&&(o&&i.tag==t.localName||!o&&!i["extends"]))return n(t,i,r)}}function n(t,n,o){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),o&&e.attached(t),e.upgradeSubtree(t,o),a.upgrade&&console.groupEnd(),t}function r(e,t){Object.__proto__?e.__proto__=t.prototype:(o(e,t.prototype,t["native"]),e.__proto__=t.prototype)}function o(e,t,n){for(var r={},o=t;o!==n&&o!==HTMLElement.prototype;){for(var i,a=Object.getOwnPropertyNames(o),s=0;i=a[s];s++)r[i]||(Object.defineProperty(e,i,Object.getOwnPropertyDescriptor(o,i)),r[i]=1);o=Object.getPrototypeOf(o)}}function i(e){e.createdCallback&&e.createdCallback()}var a=e.flags;e.upgrade=t,e.upgradeWithDefinition=n,e.implementPrototype=r}),window.CustomElements.addModule(function(e){function t(t,r){var c=r||{};if(!t)throw new Error("document.registerElement: first argument `name` must not be empty");if(t.indexOf("-")<0)throw new Error("document.registerElement: first argument ('name') must contain a dash ('-'). Argument provided was '"+String(t)+"'.");if(o(t))throw new Error("Failed to execute 'registerElement' on 'Document': Registration failed for type '"+String(t)+"'. The type name is invalid.");if(d(t))throw new Error("DuplicateDefinitionError: a type with name '"+String(t)+"' is already registered");return c.prototype||(c.prototype=Object.create(HTMLElement.prototype)),c.__name=t.toLowerCase(),c.lifecycle=c.lifecycle||{},c.ancestry=i(c["extends"]),a(c),s(c),n(c.prototype),u(c.__name,c),c.ctor=l(c),c.ctor.prototype=c.prototype,c.prototype.constructor=c.ctor,e.ready&&w(document),c.ctor}function n(e){if(!e.setAttribute._polyfilled){var t=e.setAttribute;e.setAttribute=function(e,n){ +r.call(this,e,n,t)};var n=e.removeAttribute;e.removeAttribute=function(e){r.call(this,e,null,n)},e.setAttribute._polyfilled=!0}}function r(e,t,n){e=e.toLowerCase();var r=this.getAttribute(e);n.apply(this,arguments);var o=this.getAttribute(e);this.attributeChangedCallback&&o!==r&&this.attributeChangedCallback(e,r,o)}function o(e){for(var t=0;t=0&&b(r,HTMLElement),r)}function p(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return _(e),e}}var m,v=e.isIE,w=e.upgradeDocumentTree,_=e.upgradeAll,g=e.upgradeWithDefinition,b=e.implementPrototype,y=e.useNative,E=["annotation-xml","color-profile","font-face","font-face-src","font-face-uri","font-face-format","font-face-name","missing-glyph"],L={},T="http://www.w3.org/1999/xhtml",M=document.createElement.bind(document),N=document.createElementNS.bind(document);m=Object.__proto__||y?function(e,t){return e instanceof t}:function(e,t){if(e instanceof t)return!0;for(var n=e;n;){if(n===t.prototype)return!0;n=n.__proto__}return!1},p(Node.prototype,"cloneNode"),p(document,"importNode"),v&&!function(){var e=document.importNode;document.importNode=function(){var t=e.apply(document,arguments);if(t.nodeType==t.DOCUMENT_FRAGMENT_NODE){var n=document.createDocumentFragment();return n.appendChild(t),n}return t}}(),document.registerElement=t,document.createElement=f,document.createElementNS=h,e.registry=L,e["instanceof"]=m,e.reservedTagList=E,e.getRegisteredDefinition=d,document.register=document.registerElement}),function(e){function t(){a(window.wrap(document)),window.CustomElements.ready=!0;var e=window.requestAnimationFrame||function(e){setTimeout(e,16)};e(function(){setTimeout(function(){window.CustomElements.readyTime=Date.now(),window.HTMLImports&&(window.CustomElements.elapsed=window.CustomElements.readyTime-window.HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})})}var n=e.useNative,r=e.initializeModules,o=e.isIE;if(n){var i=function(){};e.watchShadow=i,e.upgrade=i,e.upgradeAll=i,e.upgradeDocumentTree=i,e.upgradeSubtree=i,e.takeRecords=i,e["instanceof"]=function(e,t){return e instanceof t}}else r();var a=e.upgradeDocumentTree,s=e.upgradeDocument;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),window.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){e["import"]&&s(wrap(e["import"]))}),(!window.CustomEvent||o&&"function"!=typeof window.CustomEvent)&&(window.CustomEvent=function(e,t){t=t||{};var n=document.createEvent("CustomEvent");return n.initCustomEvent(e,Boolean(t.bubbles),Boolean(t.cancelable),t.detail),n.preventDefault=function(){Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})},n},window.CustomEvent.prototype=window.Event.prototype),"complete"===document.readyState||e.flags.eager)t();else if("interactive"!==document.readyState||window.attachEvent||window.HTMLImports&&!window.HTMLImports.ready){var c=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(c,t)}else t()}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){function e(e){switch(e){case"&":return"&";case"<":return"<";case">":return">";case" ":return" "}}function t(t){return t.replace(a,e)}var n="template",r=document.implementation.createHTMLDocument("template"),o=!0;HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=r.createDocumentFragment());for(var n;n=e.firstChild;)e.content.appendChild(n);if(o)try{Object.defineProperty(e,"innerHTML",{get:function(){for(var e="",n=this.content.firstChild;n;n=n.nextSibling)e+=n.outerHTML||t(n.data);return e},set:function(e){for(r.body.innerHTML=e,HTMLTemplateElement.bootstrap(r);this.content.firstChild;)this.content.removeChild(this.content.firstChild);for(;r.body.firstChild;)this.content.appendChild(r.body.firstChild)},configurable:!0})}catch(i){o=!1}},HTMLTemplateElement.bootstrap=function(e){for(var t,r=e.querySelectorAll(n),o=0,i=r.length;i>o&&(t=r[o]);o++)HTMLTemplateElement.decorate(t)},window.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var i=document.createElement;document.createElement=function(){"use strict";var e=i.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e};var a=/[&\u00A0<>]/g}(),function(e){"use strict";if(!window.performance){var t=Date.now();window.performance={now:function(){return Date.now()-t}}}window.requestAnimationFrame||(window.requestAnimationFrame=function(){var e=window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame;return e?function(t){return e(function(){t(performance.now())})}:function(e){return window.setTimeout(e,1e3/60)}}()),window.cancelAnimationFrame||(window.cancelAnimationFrame=function(){return window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||function(e){clearTimeout(e)}}())}(window.WebComponents),function(e){var t=document.createElement("style");t.textContent="body {transition: opacity ease-in 0.2s; } \nbody[unresolved] {opacity: 0; display: block; overflow: hidden; position: relative; } \n";var n=document.querySelector("head");n.insertBefore(t,n.firstChild)}(window.WebComponents); \ No newline at end of file diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 96fe2a67143..61e77247aa6 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -1,10 +1,11 @@ """ homeassistant.components.group ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to group devices that can be turned on or off. -""" +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/group.html +""" import homeassistant.core as ha from homeassistant.helpers import generate_entity_id from homeassistant.helpers.event import track_state_change diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index a723f9cbd71..a2d389a789b 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -1,8 +1,10 @@ """ homeassistant.components.history ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provide pre-made queries on top of the recorder component. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/history.html """ import re from datetime import timedelta diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index bae720db8dc..57e1875cd79 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -1,76 +1,11 @@ """ -homeassistant.components.httpinterface -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +homeassistant.components.http +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This module provides an API and a HTTP interface for debug purposes. -By default it will run on port 8123. - -All API calls have to be accompanied by an 'api_password' parameter and will -return JSON. If successful calls will return status code 200 or 201. - -Other status codes that can occur are: - - 400 (Bad Request) - - 401 (Unauthorized) - - 404 (Not Found) - - 405 (Method not allowed) - -The api supports the following actions: - -/api - GET -Returns message if API is up and running. -Example result: -{ - "message": "API running." -} - -/api/states - GET -Returns a list of entities for which a state is available -Example result: -[ - { .. state object .. }, - { .. state object .. } -] - -/api/states/ - GET -Returns the current state from an entity -Example result: -{ - "attributes": { - "next_rising": "07:04:15 29-10-2013", - "next_setting": "18:00:31 29-10-2013" - }, - "entity_id": "weather.sun", - "last_changed": "23:24:33 28-10-2013", - "state": "below_horizon" -} - -/api/states/ - POST -Updates the current state of an entity. Returns status code 201 if successful -with location header of updated resource and as body the new state. -parameter: new_state - string -optional parameter: attributes - JSON encoded object -Example result: -{ - "attributes": { - "next_rising": "07:04:15 29-10-2013", - "next_setting": "18:00:31 29-10-2013" - }, - "entity_id": "weather.sun", - "last_changed": "23:24:33 28-10-2013", - "state": "below_horizon" -} - -/api/events/ - POST -Fires an event with event_type -optional parameter: event_data - JSON encoded object -Example result: -{ - "message": "Event download_file fired." -} - +For more details about the RESTful API, please refer to the documentation at +https://home-assistant.io/developers/api.html """ - import json import threading import logging diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py index 1eacd61bcee..4a17a5046c6 100644 --- a/homeassistant/components/ifttt.py +++ b/homeassistant/components/ifttt.py @@ -1,23 +1,10 @@ """ homeassistant.components.ifttt -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This component enable you to trigger Maker IFTTT recipes. -Check https://ifttt.com/maker for details. - -Configuration: - -To use Maker IFTTT you will need to add something like the following to your -config/configuration.yaml. - -ifttt: - key: xxxxx-x-xxxxxxxxxxxxx - -Variables: - -key -*Required -Your api key +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ifttt.html """ import logging import requests diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py index 3a1af572a30..4c367703903 100644 --- a/homeassistant/components/introduction.py +++ b/homeassistant/components/introduction.py @@ -1,8 +1,10 @@ """ homeassistant.components.introduction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Component that will help guide the user taking its first steps. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/introduction.html """ import logging diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 3629fce31bf..6054547d685 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -1,8 +1,10 @@ """ homeassistant.components.keyboard ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Provides functionality to emulate keyboard presses on host machine. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/keyboard.html """ import logging diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index b438d7b92b1..eff9aef4f36 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -2,6 +2,8 @@ homeassistant.components.light.hue ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for Hue lights. + +https://home-assistant.io/components/light.hue.html """ import logging import socket diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py new file mode 100644 index 00000000000..d5fa3f9f2ce --- /dev/null +++ b/homeassistant/components/light/hyperion.py @@ -0,0 +1,125 @@ +""" +homeassistant.components.light.hyperion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Hyperion remotes. + +https://home-assistant.io/components/light.hyperion.html +""" +import logging +import socket +import json + +from homeassistant.const import CONF_HOST +from homeassistant.components.light import (Light, ATTR_RGB_COLOR) + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = [] + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Sets up a Hyperion server remote """ + host = config.get(CONF_HOST, None) + port = config.get("port", 19444) + device = Hyperion(host, port) + if device.setup(): + add_devices_callback([device]) + return True + else: + return False + + +class Hyperion(Light): + """ Represents a Hyperion remote """ + + def __init__(self, host, port): + self._host = host + self._port = port + self._name = host + self._is_available = True + self._rgb_color = [255, 255, 255] + + @property + def name(self): + """ Return the hostname of the server. """ + return self._name + + @property + def rgb_color(self): + """ Last RGB color value set. """ + return self._rgb_color + + @property + def is_on(self): + """ True if the device is online. """ + return self._is_available + + def turn_on(self, **kwargs): + """ Turn the lights on. """ + if self._is_available: + if ATTR_RGB_COLOR in kwargs: + self._rgb_color = kwargs[ATTR_RGB_COLOR] + + self.json_request({"command": "color", "priority": 128, + "color": self._rgb_color}) + + def turn_off(self, **kwargs): + """ Disconnect the remote. """ + self.json_request({"command": "clearall"}) + + def update(self): + """ Ping the remote. """ + # just see if the remote port is open + self._is_available = self.json_request() + + def setup(self): + """ Get the hostname of the remote. """ + response = self.json_request({"command": "serverinfo"}) + if response: + self._name = response["info"]["hostname"] + return True + + return False + + def json_request(self, request=None, wait_for_response=False): + """ Communicate with the json server. """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5) + + try: + sock.connect((self._host, self._port)) + except OSError: + sock.close() + return False + + if not request: + # no communication needed, simple presence detection returns True + sock.close() + return True + + sock.send(bytearray(json.dumps(request) + "\n", "utf-8")) + try: + buf = sock.recv(4096) + except socket.timeout: + # something is wrong, assume it's offline + sock.close() + return False + + # read until a newline or timeout + buffering = True + while buffering: + if "\n" in str(buf, "utf-8"): + response = str(buf, "utf-8").split("\n")[0] + buffering = False + else: + try: + more = sock.recv(4096) + except socket.timeout: + more = None + if not more: + buffering = False + response = str(buf, "utf-8") + else: + buf += more + + sock.close() + return json.loads(response) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index ba8b8235260..b35ed379047 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -12,22 +12,7 @@ Support for LimitlessLED bulbs, also known as... - dekolight - iLight -Configuration: - -To use limitlessled you will need to add the following to your -configuration.yaml file. - -light: - platform: limitlessled - bridges: - - host: 192.168.1.10 - group_1_name: Living Room - group_2_name: Bedroom - group_3_name: Office - group_3_type: white - group_4_name: Kitchen - - host: 192.168.1.11 - group_2_name: Basement +https://home-assistant.io/components/light.limitlessled.html """ import logging diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 819dce499e9..22747c4fb04 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -2,6 +2,9 @@ homeassistant.components.light.tellstick ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for Tellstick lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.tellstick.html """ import logging # pylint: disable=no-name-in-module, import-error diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index f41bfb56685..dc447f2e4b5 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -1,52 +1,10 @@ """ homeassistant.components.light.vera ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Support for Vera lights. This component is useful if you wish for switches -connected to your Vera controller to appear as lights in Home Assistant. -All switches will be added as a light unless you exclude them in the config. - -Configuration: - -To use the Vera lights you will need to add something like the following to -your configuration.yaml file. - -light: - platform: vera - vera_controller_url: http://YOUR_VERA_IP:3480/ - device_data: - 12: - name: My awesome switch - exclude: true - 13: - name: Another switch - -Variables: - -vera_controller_url -*Required -This is the base URL of your vera controller including the port number if not -running on 80. Example: http://192.168.1.21:3480/ - -device_data -*Optional -This contains an array additional device info for your Vera devices. It is not -required and if not specified all lights configured in your Vera controller -will be added with default values. You should use the id of your vera device -as the key for the device within device_data. - -These are the variables for the device_data array: - -name -*Optional -This parameter allows you to override the name of your Vera device in the HA -interface, if not specified the value configured for the device in your Vera -will be used. - -exclude -*Optional -This parameter allows you to exclude the specified device from Home Assistant, -it should be set to "true" if you want this device excluded. +Support for Vera lights. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.vera.html """ import logging from requests.exceptions import RequestException diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 98988c20688..40b5b1883fc 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -2,6 +2,9 @@ homeassistant.components.light.wink ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for Wink lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.wink.html """ import logging diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 75a5cd83823..e81baf4a49c 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -1,8 +1,10 @@ """ homeassistant.components.logbook ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Parses events and generates a human log. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/logbook.html """ from datetime import timedelta from itertools import groupby diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 294fccbb1f5..8040ef9c067 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -28,6 +28,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DISCOVERY_PLATFORMS = { discovery.SERVICE_CAST: 'cast', discovery.SERVICE_SONOS: 'sonos', + discovery.SERVICE_PLEX: 'plex', } SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 6f622c9e0cc..175c2e21094 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -3,22 +3,8 @@ homeassistant.components.media_player.chromecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to interact with Cast devices on the network. -WARNING: This platform is currently not working due to a changed Cast API. - -Configuration: - -To use the chromecast integration you will need to add something like the -following to your configuration.yaml file. - -media_player: - platform: chromecast - host: 192.168.1.9 - -Variables: - -host -*Optional -Use only if you don't want to scan for devices. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.cast.html """ import logging diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index 19286906f49..4c9065f5289 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -2,42 +2,9 @@ homeassistant.components.media_player.denon ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides an interface to Denon Network Receivers. -Developed for a Denon DRA-N5, see -http://www.denon.co.uk/chg/product/compactsystems/networkmusicsystems/ceolpiccolo -A few notes: - - As long as this module is active and connected, the receiver does - not seem to accept additional telnet connections. - - - Be careful with the volume. 50% or even 100% are very loud. - - - To be able to wake up the receiver, activate the "remote" setting - in the receiver's settings. - - - Play and pause are supported, toggling is not possible. - - - Seeking cannot be implemented as the UI sends absolute positions. - Only seeking via simulated button presses is possible. - -Configuration: - -To use your Denon you will need to add something like the following to -your config/configuration.yaml: - -media_player: - platform: denon - name: Music station - host: 192.168.0.123 - -Variables: - -host -*Required -The ip of the player. Example: 192.168.0.123 - -name -*Optional -The name of the device. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.denon.html """ import telnetlib import logging @@ -67,13 +34,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): CONF_HOST) return False - add_devices([ - DenonDevice( - config.get('name', 'Music station'), - config.get('host')) - ]) - - return True + denon = DenonDevice( + config.get("name", "Music station"), + config.get("host") + ) + if denon.update(): + add_devices([denon]) + return True + else: + return False class DenonDevice(MediaPlayerDevice): @@ -84,28 +53,41 @@ class DenonDevice(MediaPlayerDevice): def __init__(self, name, host): self._name = name self._host = host - self._telnet = telnetlib.Telnet(self._host) + self._pwstate = "PWSTANDBY" + self._volume = 0 + self._muted = False + self._mediasource = "" - def query(self, message): - """ Send request and await response from server """ + @classmethod + def telnet_request(cls, telnet, command): + """ Executes `command` and returns the response. """ + telnet.write(command.encode("ASCII") + b"\r") + return telnet.read_until(b"\r", timeout=0.2).decode("ASCII").strip() + + def telnet_command(self, command): + """ Establishes a telnet connection and sends `command`. """ + telnet = telnetlib.Telnet(self._host) + telnet.write(command.encode("ASCII") + b"\r") + telnet.read_very_eager() # skip response + telnet.close() + + def update(self): try: - # unspecified command, should be ignored - self._telnet.write("?".encode('UTF-8') + b'\r') - except (EOFError, BrokenPipeError, ConnectionResetError): - self._telnet.open(self._host) + telnet = telnetlib.Telnet(self._host) + except ConnectionRefusedError: + return False - self._telnet.read_very_eager() # skip what is not requested + self._pwstate = self.telnet_request(telnet, "PW?") + # PW? sends also SISTATUS, which is not interesting + telnet.read_until(b"\r", timeout=0.2) - self._telnet.write(message.encode('ASCII') + b'\r') - # timeout 200ms, defined by protocol - resp = self._telnet.read_until(b'\r', timeout=0.2)\ - .decode('UTF-8').strip() + volume_str = self.telnet_request(telnet, "MV?")[len("MV"):] + self._volume = int(volume_str) / 60 + self._muted = (self.telnet_request(telnet, "MU?") == "MUON") + self._mediasource = self.telnet_request(telnet, "SI?")[len("SI"):] - if message == "PW?": - # workaround; PW? sends also SISTATUS - self._telnet.read_until(b'\r', timeout=0.2) - - return resp + telnet.close() + return True @property def name(self): @@ -115,10 +97,9 @@ class DenonDevice(MediaPlayerDevice): @property def state(self): """ Returns the state of the device. """ - pwstate = self.query('PW?') - if pwstate == "PWSTANDBY": + if self._pwstate == "PWSTANDBY": return STATE_OFF - if pwstate == "PWON": + if self._pwstate == "PWON": return STATE_ON return STATE_UNKNOWN @@ -126,17 +107,17 @@ class DenonDevice(MediaPlayerDevice): @property def volume_level(self): """ Volume level of the media player (0..1). """ - return int(self.query('MV?')[len('MV'):]) / 60 + return self._volume @property def is_volume_muted(self): """ Boolean if volume is currently muted. """ - return self.query('MU?') == "MUON" + return self._muted @property def media_title(self): """ Current media source. """ - return self.query('SI?')[len('SI'):] + return self._mediasource @property def supported_media_commands(self): @@ -145,24 +126,24 @@ class DenonDevice(MediaPlayerDevice): def turn_off(self): """ turn_off media player. """ - self.query('PWSTANDBY') + self.telnet_command("PWSTANDBY") def volume_up(self): """ volume_up media player. """ - self.query('MVUP') + self.telnet_command("MVUP") def volume_down(self): """ volume_down media player. """ - self.query('MVDOWN') + self.telnet_command("MVDOWN") def set_volume_level(self, volume): """ set volume level, range 0..1. """ # 60dB max - self.query('MV' + str(round(volume * 60)).zfill(2)) + self.telnet_command("MV" + str(round(volume * 60)).zfill(2)) def mute_volume(self, mute): """ mute (true) or unmute (false) media player. """ - self.query('MU' + ('ON' if mute else 'OFF')) + self.telnet_command("MU" + ("ON" if mute else "OFF")) def media_play_pause(self): """ media_play_pause media player. """ @@ -170,22 +151,22 @@ class DenonDevice(MediaPlayerDevice): def media_play(self): """ media_play media player. """ - self.query('NS9A') + self.telnet_command("NS9A") def media_pause(self): """ media_pause media player. """ - self.query('NS9B') + self.telnet_command("NS9B") def media_next_track(self): """ Send next track command. """ - self.query('NS9D') + self.telnet_command("NS9D") def media_previous_track(self): - self.query('NS9E') + self.telnet_command("NS9E") def media_seek(self, position): raise NotImplementedError() def turn_on(self): """ turn the media player on. """ - self.query('PWON') + self.telnet_command("PWON") diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py new file mode 100644 index 00000000000..9d712c50c89 --- /dev/null +++ b/homeassistant/components/media_player/firetv.py @@ -0,0 +1,190 @@ +""" +homeassistant.components.media_player.firetv +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides functionality to interact with FireTV devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.firetv.html +""" +import logging +import requests + +from homeassistant.const import ( + STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF, + STATE_UNKNOWN, STATE_STANDBY) + +from homeassistant.components.media_player import ( + MediaPlayerDevice, + SUPPORT_PAUSE, SUPPORT_VOLUME_SET, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK) + +SUPPORT_FIRETV = SUPPORT_PAUSE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET + +DOMAIN = 'firetv' +DEVICE_LIST_URL = 'http://{0}/devices/list' +DEVICE_STATE_URL = 'http://{0}/devices/state/{1}' +DEVICE_ACTION_URL = 'http://{0}/devices/action/{1}/{2}' + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the FireTV platform. """ + host = config.get('host', 'localhost:5556') + device_id = config.get('device', 'default') + try: + response = requests.get(DEVICE_LIST_URL.format(host)).json() + if device_id in response['devices'].keys(): + add_devices([ + FireTVDevice( + host, + device_id, + config.get('name', 'Amazon Fire TV') + ) + ]) + _LOGGER.info( + 'Device %s accessible and ready for control', device_id) + else: + _LOGGER.warn( + 'Device %s is not registered with firetv-server', device_id) + except requests.exceptions.RequestException: + _LOGGER.error('Could not connect to firetv-server at %s', host) + + +class FireTV(object): + """ firetv-server client. + + Should a native Python 3 ADB module become available, python-firetv can + support Python 3, it can be added as a dependency, and this class can be + dispensed of. + + For now, it acts as a client to the firetv-server HTTP server (which must + be running via Python 2). + """ + + def __init__(self, host, device_id): + self.host = host + self.device_id = device_id + + @property + def state(self): + """ Get the device state. An exception means UNKNOWN state. """ + try: + response = requests.get( + DEVICE_STATE_URL.format( + self.host, + self.device_id + ) + ).json() + return response.get('state', STATE_UNKNOWN) + except requests.exceptions.RequestException: + _LOGGER.error( + 'Could not retrieve device state for %s', self.device_id) + return STATE_UNKNOWN + + def action(self, action_id): + """ Perform an action on the device. """ + try: + requests.get( + DEVICE_ACTION_URL.format( + self.host, + self.device_id, + action_id + ) + ) + except requests.exceptions.RequestException: + _LOGGER.error( + 'Action request for %s was not accepted for device %s', + action_id, self.device_id) + + +class FireTVDevice(MediaPlayerDevice): + """ Represents an Amazon Fire TV device on the network. """ + + def __init__(self, host, device, name): + self._firetv = FireTV(host, device) + self._name = name + self._state = STATE_UNKNOWN + + @property + def name(self): + """ Get the device name. """ + return self._name + + @property + def should_poll(self): + """ Device should be polled. """ + return True + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_FIRETV + + @property + def state(self): + """ State of the player. """ + return self._state + + def update(self): + """ Update device state. """ + self._state = { + 'idle': STATE_IDLE, + 'off': STATE_OFF, + 'play': STATE_PLAYING, + 'pause': STATE_PAUSED, + 'standby': STATE_STANDBY, + 'disconnected': STATE_UNKNOWN, + }.get(self._firetv.state, STATE_UNKNOWN) + + def turn_on(self): + """ Turns on the device. """ + self._firetv.action('turn_on') + + def turn_off(self): + """ Turns off the device. """ + self._firetv.action('turn_off') + + def media_play(self): + """ Send play command. """ + self._firetv.action('media_play') + + def media_pause(self): + """ Send pause command. """ + self._firetv.action('media_pause') + + def media_play_pause(self): + """ Send play/pause command. """ + self._firetv.action('media_play_pause') + + def volume_up(self): + """ Send volume up command. """ + self._firetv.action('volume_up') + + def volume_down(self): + """ Send volume down command. """ + self._firetv.action('volume_down') + + def media_previous_track(self): + """ Send previous track command (results in rewind). """ + self._firetv.action('media_previous') + + def media_next_track(self): + """ Send next track command (results in fast-forward). """ + self._firetv.action('media_next') + + def media_seek(self, position): + raise NotImplementedError() + + def mute_volume(self, mute): + raise NotImplementedError() + + def play_youtube(self, media_id): + raise NotImplementedError() + + def set_volume_level(self, volume): + raise NotImplementedError() diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 70def719146..40c771d1ea6 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -1,36 +1,10 @@ """ homeassistant.components.media_player.itunes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Provides an interface to iTunes-API (https://github.com/maddox/itunes-api) - -The iTunes media player will allow you to control your iTunes instance. You -can play/pause/next/previous/mute, adjust volume, etc. - -In addition to controlling iTunes, your available AirPlay endpoints will be -added as media players as well. You can then individually address them append -turn them on, turn them off, or adjust their volume. - -Configuration: - -To use iTunes you will need to add something like the following to -your configuration.yaml file. - -media_player: - platform: itunes - name: iTunes - host: http://192.168.1.16 - port: 8181 - -Variables: - -name -*Optional -The name of the device. - -url -*Required -URL of your running version of iTunes-API. Example: http://192.168.1.50:8181 +Provides an interface to iTunes API. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.itunes.html """ import logging @@ -157,11 +131,9 @@ class Itunes(object): path = '/airplay_devices/' + device_id + '/volume' return self._request('PUT', path, {'level': level}) -# pylint: disable=unused-argument -# pylint: disable=abstract-method + +# pylint: disable=unused-argument, abstract-method # pylint: disable=too-many-instance-attributes - - def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the itunes platform. """ @@ -179,7 +151,6 @@ class ItunesDevice(MediaPlayerDevice): """ Represents a iTunes-API instance. """ # pylint: disable=too-many-public-methods - def __init__(self, name, host, port, add_devices): self._name = name self._host = host diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 2fe42e2e707..8e07ec2df8b 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -3,35 +3,8 @@ homeassistant.components.media_player.kodi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides an interface to the XBMC/Kodi JSON-RPC API -Configuration: - -To use the Kodi you will need to add something like the following to -your configuration.yaml file. - -media_player: - platform: kodi - name: Kodi - url: http://192.168.0.123/jsonrpc - user: kodi - password: my_secure_password - -Variables: - -name -*Optional -The name of the device. - -url -*Required -The URL of the XBMC/Kodi JSON-RPC API. Example: http://192.168.0.123/jsonrpc - -user -*Optional -The XBMC/Kodi HTTP username. - -password -*Optional -The XBMC/Kodi HTTP password. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.kodi.html """ import urllib import logging diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 8cc22f9b982..6418cbd6e64 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -3,35 +3,8 @@ homeassistant.components.media_player.mpd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides functionality to interact with a Music Player Daemon. -Configuration: - -To use MPD you will need to add something like the following to your -configuration.yaml file. - -media_player: - platform: mpd - server: 127.0.0.1 - port: 6600 - location: bedroom - password: superSecretPassword123 - -Variables: - -server -*Required -IP address of the Music Player Daemon. Example: 192.168.1.32 - -port -*Optional -Port of the Music Player Daemon, defaults to 6600. Example: 6600 - -location -*Optional -Location of your Music Player Daemon. - -password -*Optional -Password for your Music Player Daemon. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.mpd.html """ import logging import socket diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 5fac9ecb0f0..b8267d286d3 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -6,38 +6,114 @@ Provides an interface to the Plex API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.plex.html """ +import os +import json import logging from datetime import timedelta +from urllib.parse import urlparse +from homeassistant.loader import get_component +import homeassistant.util as util from homeassistant.components.media_player import ( MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) from homeassistant.const import ( - STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) -import homeassistant.util as util + DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_PLAYING, + STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) -REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/' - 'df2d0847e801d6d5cda920326d693cf75f304f1a.zip' - '#python-plexapi==1.0.2'] +REQUIREMENTS = ['plexapi==1.1.0'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +PLEX_CONFIG_FILE = 'plex.conf' + +# Map ip to request id for configuring +_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK -# pylint: disable=abstract-method, unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the plex platform. """ - from plexapi.myplex import MyPlexUser - from plexapi.exceptions import BadRequest +def config_from_file(filename, config=None): + ''' Small configuration file management function''' + if config: + # We're writing configuration + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +# pylint: disable=abstract-method, unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Sets up the plex platform. """ + + config = config_from_file(hass.config.path(PLEX_CONFIG_FILE)) + if len(config): + # Setup a configured PlexServer + host, token = config.popitem() + token = token['token'] + # Via discovery + elif discovery_info is not None: + # Parse discovery data + host = urlparse(discovery_info[1]).netloc + _LOGGER.info('Discovered PLEX server: %s', host) + + if host in _CONFIGURING: + return + token = None + else: + return + + setup_plexserver(host, token, hass, add_devices_callback) + + +# pylint: disable=too-many-branches +def setup_plexserver(host, token, hass, add_devices_callback): + ''' Setup a plexserver based on host parameter''' + import plexapi.server + import plexapi.exceptions + + try: + plexserver = plexapi.server.PlexServer('http://%s' % host, token) + except (plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound) as error: + _LOGGER.info(error) + # No token or wrong token + request_configuration(host, hass, add_devices_callback) + return + + # If we came here and configuring this host, mark as done + if host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Discovery configuration done!') + + # Save config + if not config_from_file( + hass.config.path(PLEX_CONFIG_FILE), + {host: {'token': token}}): + _LOGGER.error('failed to save config file') + + _LOGGER.info('Connected to: htts://%s', host) - name = config.get('name', '') - user = config.get('user', '') - password = config.get('password', '') - plexuser = MyPlexUser.signin(user, password) - plexserver = plexuser.getResource(name).connect() plex_clients = {} plex_sessions = {} @@ -45,34 +121,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def update_devices(): """ Updates the devices objects. """ try: - devices = plexuser.devices() - except BadRequest: + devices = plexserver.clients() + except plexapi.exceptions.BadRequest: _LOGGER.exception("Error listing plex devices") return new_plex_clients = [] for device in devices: - if (all(x not in ['client', 'player'] for x in device.provides) - or 'PlexAPI' == device.product): + # For now, let's allow all deviceClass types + if device.deviceClass in ['badClient']: continue - if device.clientIdentifier not in plex_clients: + if device.machineIdentifier not in plex_clients: new_client = PlexClient(device, plex_sessions, update_devices, update_sessions) - plex_clients[device.clientIdentifier] = new_client + plex_clients[device.machineIdentifier] = new_client new_plex_clients.append(new_client) else: - plex_clients[device.clientIdentifier].set_device(device) + plex_clients[device.machineIdentifier].set_device(device) if new_plex_clients: - add_devices(new_plex_clients) + add_devices_callback(new_plex_clients) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_sessions(): """ Updates the sessions objects. """ try: sessions = plexserver.sessions() - except BadRequest: + except plexapi.exceptions.BadRequest: _LOGGER.exception("Error listing plex sessions") return @@ -84,10 +160,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): update_sessions() +def request_configuration(host, hass, add_devices_callback): + """ Request configuration steps from the user. """ + configurator = get_component('configurator') + + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING[host], "Failed to register, please try again.") + + return + + def plex_configuration_callback(data): + """ Actions to do when our configuration callback is called. """ + setup_plexserver(host, data.get('token'), hass, add_devices_callback) + + _CONFIGURING[host] = configurator.request_config( + hass, "Plex Media Server", plex_configuration_callback, + description=('Enter the X-Plex-Token'), + description_image="/static/images/config_plex_mediaserver.png", + submit_caption="Confirm", + fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}] + ) + + class PlexClient(MediaPlayerDevice): """ Represents a Plex device. """ - # pylint: disable=too-many-public-methods + # pylint: disable=too-many-public-methods, attribute-defined-outside-init def __init__(self, device, plex_sessions, update_devices, update_sessions): self.plex_sessions = plex_sessions self.update_devices = update_devices @@ -99,17 +199,23 @@ class PlexClient(MediaPlayerDevice): self.device = device @property - def session(self): - """ Returns the session, if any. """ - if self.device.clientIdentifier not in self.plex_sessions: - return None - - return self.plex_sessions[self.device.clientIdentifier] + def unique_id(self): + """ Returns the id of this plex client """ + return "{}.{}".format( + self.__class__, self.device.machineIdentifier or self.device.name) @property def name(self): """ Returns the name of the device. """ - return self.device.name or self.device.product or self.device.device + return self.device.name or DEVICE_DEFAULT_NAME + + @property + def session(self): + """ Returns the session, if any. """ + if self.device.machineIdentifier not in self.plex_sessions: + return None + + return self.plex_sessions[self.device.machineIdentifier] @property def state(self): @@ -120,7 +226,8 @@ class PlexClient(MediaPlayerDevice): return STATE_PLAYING elif state == 'paused': return STATE_PAUSED - elif self.device.isReachable: + # This is nasty. Need to find a way to determine alive + elif self.device: return STATE_IDLE else: return STATE_OFF @@ -196,16 +303,16 @@ class PlexClient(MediaPlayerDevice): def media_play(self): """ media_play media player. """ - self.device.play({'type': 'video'}) + self.device.play() def media_pause(self): """ media_pause media player. """ - self.device.pause({'type': 'video'}) + self.device.pause() def media_next_track(self): """ Send next track command. """ - self.device.skipNext({'type': 'video'}) + self.device.skipNext() def media_previous_track(self): """ Send previous track command. """ - self.device.skipPrevious({'type': 'video'}) + self.device.skipPrevious() diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index faf4f6aa983..13ee668b18a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -1,17 +1,11 @@ """ homeassistant.components.media_player.sonos -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides an interface to Sonos players (via SoCo) -Configuration: - -To use SoCo, add something like this to your configuration: - -media_player: - platform: sonos +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.sonos.html """ - import logging import datetime @@ -56,8 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return True -# pylint: disable=too-many-instance-attributes -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-instance-attributes, too-many-public-methods # pylint: disable=abstract-method class SonosDevice(MediaPlayerDevice): """ Represents a Sonos device. """ @@ -74,7 +67,7 @@ class SonosDevice(MediaPlayerDevice): return True def update_sonos(self, now): - """ Updates state, called by track_utc_time_change """ + """ Updates state, called by track_utc_time_change. """ self.update_ha_state(True) @property @@ -162,31 +155,31 @@ class SonosDevice(MediaPlayerDevice): return SUPPORT_SONOS def turn_off(self): - """ turn_off media player. """ + """ Turn off media player. """ self._player.pause() def volume_up(self): - """ volume_up media player. """ + """ Volume up media player. """ self._player.volume += 1 def volume_down(self): - """ volume_down media player. """ + """ Volume down media player. """ self._player.volume -= 1 def set_volume_level(self, volume): - """ set volume level, range 0..1. """ + """ Set volume level, range 0..1. """ self._player.volume = str(int(volume * 100)) def mute_volume(self, mute): - """ mute (true) or unmute (false) media player. """ + """ Mute (true) or unmute (false) media player. """ self._player.mute = mute def media_play(self): - """ media_play media player. """ + """ Send paly command. """ self._player.play() def media_pause(self): - """ media_pause media player. """ + """ Send pause command. """ self._player.pause() def media_next_track(self): @@ -202,5 +195,5 @@ class SonosDevice(MediaPlayerDevice): self._player.seek(str(datetime.timedelta(seconds=int(position)))) def turn_on(self): - """ turn the media player on. """ + """ Turn the media player on. """ self._player.play() diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 940aa890f3a..b1f3f16567d 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -1,39 +1,11 @@ """ homeassistant.components.media_player.squeezebox -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides an interface to the Logitech SqueezeBox API -Configuration: - -To use SqueezeBox add something something like the following to your -configuration.yaml file. - -media_player: - platform: squeezebox - host: 192.168.1.21 - port: 9090 - username: user - password: password - -Variables: - -host -*Required -The host name or address of the Logitech Media Server. - -port -*Optional -Telnet port to Logitech Media Server, default 9090. - -usermame -*Optional -Username, if password protection is enabled. - -password -*Optional -Password, if password protection is enabled. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.squeezebox.html """ - import logging import telnetlib import urllib.parse @@ -291,7 +263,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): def media_pause(self): """ media_pause media player. """ - self._lms.query(self._id, 'pause', '0') + self._lms.query(self._id, 'pause', '1') self.update_ha_state() def media_next_track(self): diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 844e59ea189..19571518112 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -3,27 +3,8 @@ homeassistant.components.modbus ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Modbus component, using pymodbus (python3 branch). -Configuration: - -To use the Modbus component you will need to add something like the following -to your configuration.yaml file. - -#Modbus TCP -modbus: - type: tcp - host: 127.0.0.1 - port: 2020 - -#Modbus RTU -modbus: - type: serial - method: rtu - port: /dev/ttyUSB0 - baudrate: 9600 - stopbits: 1 - bytesize: 8 - parity: N - +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/modbus.html """ import logging diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 71ba0fe0c9c..69b37ffc94a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,52 +1,10 @@ """ homeassistant.components.mqtt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -MQTT component, using paho-mqtt. This component needs a MQTT broker like -Mosquitto or Mosca. The Eclipse Foundation is running a public MQTT server -at iot.eclipse.org. If you prefer to use that one, keep in mind to adjust -the topic/client ID and that your messages are public. +MQTT component, using paho-mqtt. -Configuration: - -To use MQTT you will need to add something like the following to your -config/configuration.yaml. - -mqtt: - broker: 127.0.0.1 - -Or, if you want more options: - -mqtt: - broker: 127.0.0.1 - port: 1883 - client_id: home-assistant-1 - keepalive: 60 - username: your_username - password: your_secret_password - certificate: /home/paulus/dev/addtrustexternalcaroot.crt - -Variables: - -broker -*Required -This is the IP address of your MQTT broker, e.g. 192.168.1.32. - -port -*Optional -The network port to connect to. Default is 1883. - -client_id -*Optional -Client ID that Home Assistant will use. Has to be unique on the server. -Default is a random generated one. - -keepalive -*Optional -The keep alive in seconds for this client. Default is 60. - -certificate -*Optional -Certificate to use for encrypting the connection to the broker. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mqtt.html """ import logging import os diff --git a/homeassistant/components/notify/file.py b/homeassistant/components/notify/file.py index 9c0beca14ac..3fe6d555524 100644 --- a/homeassistant/components/notify/file.py +++ b/homeassistant/components/notify/file.py @@ -3,26 +3,8 @@ homeassistant.components.notify.file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ File notification service. -Configuration: - -To use the File notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: file - filename: FILENAME - timestamp: 1 or 0 - -Variables: - -filename -*Required -Name of the file to use. The file will be created if it doesn't exist and saved -in your config/ folder. - -timestamp -*Required -Add a timestamp to the entry, valid entries are 1 or 0. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.file.html """ import logging import os diff --git a/homeassistant/components/notify/instapush.py b/homeassistant/components/notify/instapush.py index 95ff0d41435..839eac24a0d 100644 --- a/homeassistant/components/notify/instapush.py +++ b/homeassistant/components/notify/instapush.py @@ -3,52 +3,8 @@ homeassistant.components.notify.instapush ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Instapush notification service. -Configuration: - -To use the Instapush notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: instapush - api_key: YOUR_APP_KEY - app_secret: YOUR_APP_SECRET - event: YOUR_EVENT - tracker: YOUR_TRACKER - -Variables: - -api_key -*Required -To retrieve this value log into your account at https://instapush.im and go -to 'APPS', choose an app, and check 'Basic Info'. - -app_secret -*Required -To get this value log into your account at https://instapush.im and go to -'APPS'. The 'Application ID' can be found under 'Basic Info'. - -event -*Required -To retrieve a valid event log into your account at https://instapush.im and go -to 'APPS'. If you have no events to use with Home Assistant, create one event -for your app. - -tracker -*Required -To retrieve the tracker value log into your account at https://instapush.im and -go to 'APPS', choose the app, and check the event entries. - -Example usage of Instapush if you have an event 'notification' and a tracker -'home-assistant'. - -curl -X POST \ - -H "x-instapush-appid: YOUR_APP_KEY" \ - -H "x-instapush-appsecret: YOUR_APP_SECRET" \ - -H "Content-Type: application/json" \ - -d '{"event":"notification","trackers":{"home-assistant":"Switch 1"}}' \ - https://api.instapush.im/v1/post - -Details for the API : https://instapush.im/developer/rest +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.instapush.html """ import logging import json diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index bf8fb2162a8..a9fa6559e71 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -3,23 +3,8 @@ homeassistant.components.notify.nma ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ NMA (Notify My Android) notification service. -Configuration: - -To use the NMA notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: nma - api_key: YOUR_API_KEY - -Variables: - -api_key -*Required -Enter the API key for NMA. Go to https://www.notifymyandroid.com and create a -new API key to use with Home Assistant. - -Details for the API : https://www.notifymyandroid.com/api.jsp +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.nma.html """ import logging import xml.etree.ElementTree as ET diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 76eaf5c0c37..49aaccd3004 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -3,21 +3,8 @@ homeassistant.components.notify.pushbullet ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PushBullet platform for notify component. -Configuration: - -To use the PushBullet notifier you will need to add something like the -following to your configuration.yaml file. - -notify: - platform: pushbullet - api_key: YOUR_API_KEY - -Variables: - -api_key -*Required -Enter the API key for PushBullet. Go to https://www.pushbullet.com/ to retrieve -your API key. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.pushbullet.html """ import logging diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index c52e430ac9f..e2b4b4c4b40 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -3,35 +3,8 @@ homeassistant.components.notify.pushover ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pushover platform for notify component. -Configuration: - -To use the Pushover notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: pushover - api_key: ABCDEFGHJKLMNOPQRSTUVXYZ - user_key: ABCDEFGHJKLMNOPQRSTUVXYZ - -Variables: - -api_key -*Required -This parameter is optional but should be configured, in order to get an API -key you should go to https://pushover.net and register a new application. - -This is a quote from the pushover website regarding free/open source apps: -"If you are creating a client-side library, application, or open source project -that will be redistributed and installed by end-users, you may want to require -each of your users to register their own application rather than including your -own API token with the software." - -When setting up the application I recommend using the icon located here: -https://home-assistant.io/images/favicon-192x192.png - -user_key -*Required -To retrieve this value log into your account at https://pushover.net +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.pushover.html """ import logging diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index bd3a2b71c0c..0b168ed5075 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -3,27 +3,8 @@ homeassistant.components.notify.slack ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Slack platform for notify component. -Configuration: - -To use the Slack notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: slack - api_key: ABCDEFGHJKLMNOPQRSTUVXYZ - default_channel: '#general' - -Variables: - -api_key -*Required -The slack API token to use for sending slack messages. -You can get your slack API token here https://api.slack.com/web?sudo=1 - -default_channel -*Required -The default channel to post to if no channel is explicitly specified when -sending the notification message. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.slack.html """ import logging diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index fbddd8d1d26..758e7839e50 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -3,54 +3,8 @@ homeassistant.components.notify.smtp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Mail (SMTP) notification service. -Configuration: - -To use the smtp notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: smtp - server: MAIL_SERVER - port: YOUR_SMTP_PORT - sender: SENDER_EMAIL_ADDRESS - starttls: 1 or 0 - username: YOUR_SMTP_USERNAME - password: YOUR_SMTP_PASSWORD - recipient: YOUR_RECIPIENT - -Variables: - -server -*Required -SMTP server which is used to end the notifications. For Google Mail, eg. -smtp.gmail.com. Keep in mind that Google has some extra layers of protection -which need special attention (Hint: 'Less secure apps'). - -port -*Required -The port that the SMTP server is using, eg. 587 for Google Mail and STARTTLS -or 465/993 depending on your SMTP servers. - -sender -*Required -E-Mail address of the sender. - -starttls -*Optional -Enables STARTTLS, eg. 1 or 0. - -username -*Required -Username for the SMTP account. - -password -*Required -Password for the SMTP server that belongs to the given username. If the -password contains a colon it need to be wrapped in apostrophes. - -recipient -*Required -Recipient of the notification. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.smtp.html """ import logging import smtplib diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 5d246f2fd0d..7881e68476d 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -3,31 +3,8 @@ homeassistant.components.notify.syslog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Syslog notification service. -Configuration: - -To use the Syslog notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: syslog - facility: SYSLOG_FACILITY - option: SYSLOG_LOG_OPTION - priority: SYSLOG_PRIORITY - -Variables: - -facility -*Optional -Facility according to RFC 3164 (http://tools.ietf.org/html/rfc3164). Default -is 'syslog' if no value is given. - -option -*Option -Log option. Default is 'pid' if no value is given. - -priority -*Optional -Priority of the messages. Default is 'info' if no value is given. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.syslog.html """ import logging import syslog diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 1d72f6a262b..d4f5a7336a6 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -3,31 +3,8 @@ homeassistant.components.notify.xmpp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Jabber (XMPP) notification service. -Configuration: - -To use the Jabber notifier you will need to add something like the following -to your configuration.yaml file. - -notify: - platform: xmpp - sender: YOUR_JID - password: YOUR_JABBER_ACCOUNT_PASSWORD - recipient: YOUR_RECIPIENT - -Variables: - -sender -*Required -The Jabber ID (JID) that will act as origin of the messages. Add your JID -including the domain, e.g. your_name@jabber.org. - -password -*Required -The password for your given Jabber account. - -recipient -*Required -The Jabber ID (JID) that will receive the messages. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.xmpp.html """ import logging diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 10f6576d23f..c6f7edd6ca8 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -1,9 +1,11 @@ """ homeassistant.components.recorder ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component that records all events and state changes. Allows other components +to query this database. -Component that records all events and state changes. -Allows other components to query this database. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/recorder.html """ import logging import threading diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 0788986c91d..9c9cf0149e8 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -3,7 +3,7 @@ homeassistant.components.rfxtrx ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides support for RFXtrx components. -For more details about this platform, please refer to the documentation at +For more details about this component, please refer to the documentation at https://home-assistant.io/components/rfxtrx.html """ import logging diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 4a85adefd17..48d7389b454 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -1,39 +1,38 @@ """ homeassistant.components.scene ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows users to set and activate scenes. -Allows users to set and activate scenes within Home Assistant. - -A scene is a set of states that describe how you want certain entities to be. -For example, light A should be red with 100 brightness. Light B should be on. - -A scene is active if all states of the scene match the real states. - -If a scene is manually activated it will store the previous state of the -entities. These will be restored when the state is deactivated manually. - -If one of the enties that are being tracked change state on its own, the -old state will not be restored when it is being deactivated. +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/scene.html """ import logging from collections import namedtuple from homeassistant.core import State -from homeassistant.helpers.event import track_state_change -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import reproduce_state from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, STATE_OFF, STATE_ON, SERVICE_TURN_ON) DOMAIN = 'scene' DEPENDENCIES = ['group'] - -ATTR_ACTIVE_REQUESTED = "active_requested" +STATE = 'scening' CONF_ENTITIES = "entities" -SceneConfig = namedtuple('SceneConfig', ['name', 'states', 'fuzzy_match']) +SceneConfig = namedtuple('SceneConfig', ['name', 'states']) + + +def activate(hass, entity_id=None): + """ Activate a scene. """ + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def setup(hass, config): @@ -43,8 +42,9 @@ def setup(hass, config): scene_configs = config.get(DOMAIN) - if not isinstance(scene_configs, list): - logger.error('Scene config should be a list of scenes') + if not isinstance(scene_configs, list) or \ + any(not isinstance(item, dict) for item in scene_configs): + logger.error('Scene config should be a list of dictionaries') return False component = EntityComponent(logger, DOMAIN, hass) @@ -57,12 +57,8 @@ def setup(hass, config): target_scenes = component.extract_from_service(service) for scene in target_scenes: - if service.service == SERVICE_TURN_ON: - scene.turn_on() - else: - scene.turn_off() + scene.activate() - hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_scene_service) hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_scene_service) return True @@ -72,14 +68,6 @@ def _process_config(scene_config): """ Process passed in config into a format to work with. """ name = scene_config.get('name') - fuzzy_match = scene_config.get('fuzzy_match') - if fuzzy_match: - # default to 1% - if isinstance(fuzzy_match, int): - fuzzy_match /= 100.0 - else: - fuzzy_match = 0.01 - states = {} c_entities = dict(scene_config.get(CONF_ENTITIES, {})) @@ -100,23 +88,16 @@ def _process_config(scene_config): states[entity_id.lower()] = State(entity_id, state, attributes) - return SceneConfig(name, states, fuzzy_match) + return SceneConfig(name, states) -class Scene(ToggleEntity): +class Scene(Entity): """ A scene is a group of entities and the states we want them to be. """ def __init__(self, hass, scene_config): self.hass = hass self.scene_config = scene_config - self.is_active = False - self.prev_states = None - self.ignore_updates = False - - track_state_change( - self.hass, self.entity_ids, self.entity_state_changed) - self.update() @property @@ -128,8 +109,8 @@ class Scene(ToggleEntity): return self.scene_config.name @property - def is_on(self): - return self.is_active + def state(self): + return STATE @property def entity_ids(self): @@ -141,82 +122,8 @@ class Scene(ToggleEntity): """ Scene state attributes. """ return { ATTR_ENTITY_ID: list(self.entity_ids), - ATTR_ACTIVE_REQUESTED: self.prev_states is not None, } - def turn_on(self): + def activate(self): """ Activates scene. Tries to get entities into requested state. """ - self.prev_states = tuple(self.hass.states.get(entity_id) - for entity_id in self.entity_ids) - - self._reproduce_state(self.scene_config.states.values()) - - def turn_off(self): - """ Deactivates scene and restores old states. """ - if self.prev_states: - self._reproduce_state(self.prev_states) - self.prev_states = None - - def entity_state_changed(self, entity_id, old_state, new_state): - """ Called when an entity part of this scene changes state. """ - if self.ignore_updates: - return - - # If new state is not what we expect, it can never be active - if self._state_as_requested(new_state): - self.update() - else: - self.is_active = False - self.prev_states = None - - self.update_ha_state() - - def update(self): - """ - Update if the scene is active. - - Will look at each requested state and see if the current entity - has the same state and has at least the same attributes with the - same values. The real state can have more attributes. - """ - self.is_active = all( - self._state_as_requested(self.hass.states.get(entity_id)) - for entity_id in self.entity_ids) - - def _state_as_requested(self, cur_state): - """ Returns if given state is as requested. """ - state = self.scene_config.states.get(cur_state and cur_state.entity_id) - - return (cur_state is not None and state.state == cur_state.state and - all(self._compare_state_attribites( - value, cur_state.attributes.get(key)) - for key, value in state.attributes.items())) - - def _fuzzy_attribute_compare(self, attr_a, attr_b): - """ - Compare the attributes passed, use fuzzy logic if they are floats. - """ - - if not (isinstance(attr_a, float) and isinstance(attr_b, float)): - return False - diff = abs(attr_a - attr_b) / (abs(attr_a) + abs(attr_b)) - return diff <= self.scene_config.fuzzy_match - - def _compare_state_attribites(self, attr1, attr2): - """ Compare the attributes passed, using fuzzy logic if specified. """ - if attr1 == attr2: - return True - if not self.scene_config.fuzzy_match: - return False - if isinstance(attr1, list): - return all(self._fuzzy_attribute_compare(a, b) - for a, b in zip(attr1, attr2)) - return self._fuzzy_attribute_compare(attr1, attr2) - - def _reproduce_state(self, states): - """ Wraps reproduce state with Scence specific logic. """ - self.ignore_updates = True - reproduce_state(self.hass, states, True) - self.ignore_updates = False - - self.update_ha_state(True) + reproduce_state(self.hass, self.scene_config.states.values(), True) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index c4f70b6d6d3..3f892fdfa80 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -1,161 +1,207 @@ """ homeassistant.components.script ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -entity_id Scripts are a sequence of actions that can be triggered manually by the user or automatically based upon automation events, etc. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/script.html """ import logging from datetime import timedelta import homeassistant.util.dt as date_util +from itertools import islice import threading -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.util import split_entity_id from homeassistant.const import ( - STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, EVENT_TIME_CHANGED) + ATTR_ENTITY_ID, EVENT_TIME_CHANGED, STATE_ON, SERVICE_TURN_ON, + SERVICE_TURN_OFF) DOMAIN = "script" +ENTITY_ID_FORMAT = DOMAIN + '.{}' DEPENDENCIES = ["group"] +STATE_NOT_RUNNING = 'Not Running' + CONF_ALIAS = "alias" -CONF_SERVICE = "execute_service" +CONF_SERVICE = "service" +CONF_SERVICE_OLD = "execute_service" CONF_SERVICE_DATA = "service_data" CONF_SEQUENCE = "sequence" CONF_EVENT = "event" CONF_EVENT_DATA = "event_data" CONF_DELAY = "delay" -ATTR_ENTITY_ID = "entity_id" + +ATTR_LAST_ACTION = 'last_action' _LOGGER = logging.getLogger(__name__) +def is_on(hass, entity_id): + """ Returns if the switch is on based on the statemachine. """ + return hass.states.is_state(entity_id, STATE_ON) + + +def turn_on(hass, entity_id): + """ Turn script on. """ + _, object_id = split_entity_id(entity_id) + + hass.services.call(DOMAIN, object_id) + + +def turn_off(hass, entity_id): + """ Turn script on. """ + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + def setup(hass, config): """ Load the scripts from the configuration. """ - scripts = [] + component = EntityComponent(_LOGGER, DOMAIN, hass) + + def service_handler(service): + """ Execute a service call to script.