diff --git a/.coveragerc b/.coveragerc
index 0147c9cc5b6..756c82f755e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -7,6 +7,9 @@ omit =
homeassistant/external/*
# omit pieces of code that rely on external devices being present
+ homeassistant/components/arduino.py
+ homeassistant/components/*/arduino.py
+
homeassistant/components/wink.py
homeassistant/components/*/wink.py
@@ -32,13 +35,16 @@ omit =
homeassistant/components/light/hue.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/mpd.py
+ homeassistant/components/notify/file.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/notify/smtp.py
+ homeassistant/components/notify/syslog.py
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/bitcoin.py
+ homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/sabnzbd.py
diff --git a/README.md b/README.md
index 05fe01340bc..7c6997e4750 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master)
+# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master) [](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This is the source code for Home Assistant. For installation instructions, tutorials and the docs, please see [the website](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
@@ -8,7 +8,7 @@ It offers the following functionality through built-in components:
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index))
* Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
- * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/)
+ * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and XBMC/Kodi (http://kodi.tv/)
* 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/), and [Modbus](http://www.modbus.org/)
* Track running system services and monitoring your system stats (Memory, disk usage, and more)
* Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands
diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example
index c99f760f21f..5acca361a30 100644
--- a/config/configuration.yaml.example
+++ b/config/configuration.yaml.example
@@ -159,5 +159,5 @@ scene:
light.tv_back_light: on
light.ceiling:
state: on
- color: [0.33, 0.66]
+ xy_color: [0.33, 0.66]
brightness: 200
diff --git a/config/custom_components/example.py b/config/custom_components/example.py
index dc18aae4b98..5bfb03353e0 100644
--- a/config/custom_components/example.py
+++ b/config/custom_components/example.py
@@ -8,6 +8,22 @@ Example component to target an entity_id to:
- turn it off if all lights are turned off
- turn it off if all people leave the house
- offer a service to turn it on for 10 seconds
+
+Configuration:
+
+To use the Example custom component you will need to add the following to
+your config/configuration.yaml
+
+example:
+ target: TARGET_ENTITY
+
+Variable:
+
+target
+*Required
+TARGET_ENTITY should be one of your devices that can be turned on and off,
+ie a light or a switch. Example value could be light.Ceiling or switch.AC
+(if you have these devices with those names).
"""
import time
import logging
@@ -31,6 +47,7 @@ CONF_TARGET = 'target'
# Name of the service that we expose
SERVICE_FLASH = 'flash'
+# Shortcut for the logger
_LOGGER = logging.getLogger(__name__)
diff --git a/config/custom_components/hello_world.py b/config/custom_components/hello_world.py
index be1b935c8ad..96d9a788b6b 100644
--- a/config/custom_components/hello_world.py
+++ b/config/custom_components/hello_world.py
@@ -3,6 +3,14 @@ custom_components.hello_world
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements the bare minimum that a component should implement.
+
+Configuration:
+
+To use the hello_word component you will need to add the following to your
+config/configuration.yaml
+
+hello_world:
+
"""
# The domain of your component. Should be equal to the name of your component
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index ee2ee54df79..f8c595255d9 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -186,6 +186,24 @@ def from_config_file(config_path, hass=None):
def enable_logging(hass):
""" Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO)
+ fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
+ "[%(name)s] %(message)s%(reset)s")
+ try:
+ from colorlog import ColoredFormatter
+ logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
+ fmt,
+ datefmt='%y-%m-%d %H:%M:%S',
+ reset=True,
+ log_colors={
+ 'DEBUG': 'cyan',
+ 'INFO': 'green',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'red',
+ }
+ ))
+ except ImportError:
+ _LOGGER.warn("Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path('home-assistant.log')
@@ -202,7 +220,7 @@ def enable_logging(hass):
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
- datefmt='%H:%M %d-%m-%y'))
+ datefmt='%y-%m-%d %H:%M:%S'))
logging.getLogger('').addHandler(err_handler)
else:
diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py
new file mode 100644
index 00000000000..e7131f9c9e0
--- /dev/null
+++ b/homeassistant/components/arduino.py
@@ -0,0 +1,134 @@
+"""
+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 config/configuration.yaml
+
+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.
+"""
+import logging
+
+from PyMata.pymata import PyMata
+import serial
+
+from homeassistant.helpers import validate_config
+from homeassistant.const import (EVENT_HOMEASSISTANT_START,
+ EVENT_HOMEASSISTANT_STOP)
+
+DOMAIN = "arduino"
+DEPENDENCIES = []
+BOARD = None
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup(hass, config):
+ """ Setup the Arduino component. """
+
+ if not validate_config(config,
+ {DOMAIN: ['port']},
+ _LOGGER):
+ return False
+
+ # pylint: disable=global-statement
+ global BOARD
+ try:
+ BOARD = ArduinoBoard(config[DOMAIN]['port'])
+ except (serial.serialutil.SerialException, FileNotFoundError):
+ _LOGGER.exception("Your port is not accessible.")
+ return False
+
+ if BOARD.get_firmata()[1] <= 2:
+ _LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
+ return False
+
+ def stop_arduino(event):
+ """ Stop the Arduino service. """
+ BOARD.disconnect()
+
+ def start_arduino(event):
+ """ Start the Arduino service. """
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino)
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino)
+
+ return True
+
+
+class ArduinoBoard(object):
+ """ Represents an Arduino board. """
+
+ def __init__(self, port):
+ self._port = port
+ self._board = PyMata(self._port, verbose=False)
+
+ def set_mode(self, pin, direction, mode):
+ """ Sets the mode and the direction of a given pin. """
+ if mode == 'analog' and direction == 'in':
+ self._board.set_pin_mode(pin,
+ self._board.INPUT,
+ self._board.ANALOG)
+ elif mode == 'analog' and direction == 'out':
+ self._board.set_pin_mode(pin,
+ self._board.OUTPUT,
+ self._board.ANALOG)
+ elif mode == 'digital' and direction == 'in':
+ self._board.set_pin_mode(pin,
+ self._board.OUTPUT,
+ self._board.DIGITAL)
+ elif mode == 'digital' and direction == 'out':
+ self._board.set_pin_mode(pin,
+ self._board.OUTPUT,
+ self._board.DIGITAL)
+ elif mode == 'pwm':
+ self._board.set_pin_mode(pin,
+ self._board.OUTPUT,
+ self._board.PWM)
+
+ def get_analog_inputs(self):
+ """ Get the values from the pins. """
+ self._board.capability_query()
+ return self._board.get_analog_response_table()
+
+ def set_digital_out_high(self, pin):
+ """ Sets a given digital pin to high. """
+ self._board.digital_write(pin, 1)
+
+ def set_digital_out_low(self, pin):
+ """ Sets a given digital pin to low. """
+ self._board.digital_write(pin, 0)
+
+ def get_digital_in(self, pin):
+ """ Gets the value from a given digital pin. """
+ self._board.digital_read(pin)
+
+ def get_analog_in(self, pin):
+ """ Gets the value from a given analog pin. """
+ self._board.analog_read(pin)
+
+ def get_firmata(self):
+ """ Return the version of the Firmata firmware. """
+ return self._board.get_firmata_version()
+
+ def disconnect(self):
+ """ Disconnects the board and closes the serial connection. """
+ self._board.reset()
+ self._board.close()
diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py
index 2abae8095d6..07f04dc7aeb 100644
--- a/homeassistant/components/demo.py
+++ b/homeassistant/components/demo.py
@@ -93,17 +93,17 @@ def setup(hass, config):
# Setup fake device tracker
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
- "http://graph.facebook.com/schoutsen/picture"})
+ "http://graph.facebook.com/297400035/picture"})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_ENTITY_PICTURE:
- "http://graph.facebook.com/anne.t.frederiksen/picture"})
+ "http://graph.facebook.com/621994601/picture"})
hass.states.set("group.all_devices", "home",
{
"auto": True,
ATTR_ENTITY_ID: [
- "device_tracker.Paulus",
- "device_tracker.Anne_Therese"
+ "device_tracker.paulus",
+ "device_tracker.anne_therese"
]
})
diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py
new file mode 100755
index 00000000000..24d170a5de7
--- /dev/null
+++ b/homeassistant/components/device_tracker/tplink.py
@@ -0,0 +1,117 @@
+"""
+homeassistant.components.device_tracker.tplink
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Device tracker platform that supports scanning a TP-Link router for device
+presence.
+
+Configuration:
+
+To use the TP-Link tracker you will need to add something like the following
+to your config/configuration.yaml
+
+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.
+
+"""
+import logging
+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 TP-Link scanner. """
+ if not validate_config(config,
+ {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
+ _LOGGER):
+ return None
+
+ scanner = TplinkDeviceScanner(config[DOMAIN])
+
+ return scanner if scanner.success_init else None
+
+
+class TplinkDeviceScanner(object):
+ """ This class queries a wireless router running TP-Link firmware
+ for connected devices.
+ """
+
+ def __init__(self, config):
+ host = config[CONF_HOST]
+ username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
+
+ self.parse_macs = re.compile('[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-' +
+ '[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}')
+
+ self.host = host
+ self.username = username
+ self.password = password
+
+ self.last_results = {}
+ self.lock = threading.Lock()
+ self.success_init = self._update_info()
+
+ def scan_devices(self):
+ """ Scans for new devices and return a
+ list containing found device ids. """
+
+ self._update_info()
+
+ return self.last_results
+
+ # pylint: disable=no-self-use
+ def get_device_name(self, device):
+ """ The TP-Link firmware doesn't save the name of the wireless
+ device. """
+
+ return None
+
+ @Throttle(MIN_TIME_BETWEEN_SCANS)
+ def _update_info(self):
+ """ Ensures the information from the TP-Link router is up to date.
+ Returns boolean if scanning successful. """
+
+ with self.lock:
+ _LOGGER.info("Loading wireless clients...")
+
+ url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
+ referer = 'http://{}'.format(self.host)
+ page = requests.get(url, auth=(self.username, self.password),
+ headers={'referer': referer})
+
+ result = self.parse_macs.findall(page.text)
+
+ if result:
+ self.last_results = [mac.replace("-", ":") for mac in result]
+ return True
+
+ return False
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 8ff722e41b2..2892e278c5c 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -20,13 +20,21 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
_LOGGER = logging.getLogger(__name__)
+FRONTEND_URLS = [
+ URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
+STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
+
+
def setup(hass, config):
""" Setup serving the frontend. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
- hass.http.register_path('GET', URL_ROOT, _handle_get_root, False)
+ for url in FRONTEND_URLS:
+ hass.http.register_path('GET', url, _handle_get_root, False)
+
+ hass.http.register_path('GET', STATES_URL, _handle_get_root, False)
# Static files
hass.http.register_path(
diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py
index d47eb125209..c388a23c50c 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 = "24f15feebc48785ce908064dccbdb204"
+VERSION = "edce0feb9f77dd8b0bbe3c9b1e749fe0"
diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html
index 446985ade55..72ed56794b7 100644
--- a/homeassistant/components/frontend/www_static/frontend.html
+++ b/homeassistant/components/frontend/www_static/frontend.html
@@ -5646,7 +5646,6 @@ this._removeChildren();
}
});
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -16059,185 +14226,13 @@ The `aria-labelledby` attribute will be set to the header element, if one exists
-
+
+
+
+
+
+
+
@@ -25768,7 +23771,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
this.brightnessSliderValue = newVal.attributes.brightness;
}
- this.debounce('more-info-light-animation-finish', function() {
+ this.async(function() {
this.fire('iron-resize');
}.bind(this), 500);
},
@@ -25810,8 +23813,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
text-transform: capitalize;
}
- /* Accent the power button because the user should use that first */
- paper-icon-button[focus] {
+ paper-icon-button[highlight] {
color: var(--accent-color);
}
@@ -25823,7 +23825,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
transition: max-height .5s ease-in;
}
- .has-media_volume .volume {
+ .has-volume_level .volume {
max-height: 40px;
}
@@ -25831,19 +23833,19 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
-
@@ -25854,7 +23856,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
(function() {
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
- var ATTRIBUTE_CLASSES = ['media_volume'];
+ var ATTRIBUTE_CLASSES = ['volume_level'];
Polymer({
is: 'more-info-media_player',
@@ -25865,9 +23867,14 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
observer: 'stateObjChanged',
},
- isIdle: {
+ isOff: {
type: Boolean,
- computed: 'computeIsIdle(stateObj)',
+ value: false,
+ },
+
+ isPlaying: {
+ type: Boolean,
+ value: false,
},
isMuted: {
@@ -25878,53 +23885,98 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
volumeSliderValue: {
type: Number,
value: 0,
- }
+ },
+
+ supportsPause: {
+ type: Boolean,
+ value: false,
+ },
+
+ supportsVolumeSet: {
+ type: Boolean,
+ value: false,
+ },
+
+ supportsVolumeMute: {
+ type: Boolean,
+ value: false,
+ },
+
+ supportsPreviousTrack: {
+ type: Boolean,
+ value: false,
+ },
+
+ supportsNextTrack: {
+ type: Boolean,
+ value: false,
+ },
+
+ supportsTurnOn: {
+ type: Boolean,
+ value: false,
+ },
+
+ supportsTurnOff: {
+ type: Boolean,
+ value: false,
+ },
+
},
- stateObjChanged: function(newVal, oldVal) {
+ stateObjChanged: function(newVal) {
if (newVal) {
- this.volumeSliderValue = newVal.attributes.media_volume * 100;
- this.isMuted = newVal.attributes.media_is_volume_muted;
+ this.isOff = newVal.state == 'off';
+ this.isPlaying = newVal.state == 'playing';
+ this.volumeSliderValue = newVal.attributes.volume_level * 100;
+ this.isMuted = newVal.attributes.is_volume_muted;
+ this.supportsPause = (newVal.attributes.supported_media_commands & 1) !== 0;
+ this.supportsVolumeSet = (newVal.attributes.supported_media_commands & 4) !== 0;
+ this.supportsVolumeMute = (newVal.attributes.supported_media_commands & 8) !== 0;
+ this.supportsPreviousTrack = (newVal.attributes.supported_media_commands & 16) !== 0;
+ this.supportsNextTrack = (newVal.attributes.supported_media_commands & 32) !== 0;
+ this.supportsTurnOn = (newVal.attributes.supported_media_commands & 128) !== 0;
+ this.supportsTurnOff = (newVal.attributes.supported_media_commands & 256) !== 0;
}
- this.debounce('more-info-volume-animation-finish', function() {
- this.fire('iron-resize');
- }.bind(this), 500);
+ this.async(function() { this.fire('iron-resize'); }.bind(this), 500);
},
computeClassNames: function(stateObj) {
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
},
- computeMediaState: function(stateObj) {
- return stateObj.state == 'idle' ? 'idle' : stateObj.attributes.media_state;
- },
-
- computeIsIdle: function(stateObj) {
- return stateObj.state == 'idle';
- },
-
- computePowerButtonCaption: function(isIdle) {
- return isIdle ? 'Turn on' : 'Turn off';
+ computeIsOff: function(stateObj) {
+ return stateObj.state == 'off';
},
computeMuteVolumeIcon: function(isMuted) {
return isMuted ? 'av:volume-off' : 'av:volume-up';
},
- computePlayPauseIcon: function(stateObj) {
- return stateObj.attributes.media_state == 'playing' ? 'av:pause' : 'av:play-arrow';
+ computePlaybackControlIcon: function(stateObj) {
+ if (this.isPlaying) {
+ return this.supportsPause ? 'av:pause' : 'av:stop';
+ }
+ return 'av:play-arrow';
+ },
+
+ computeHidePowerButton: function(isOff, supportsTurnOn, supportsTurnOff) {
+ return isOff ? !supportsTurnOn : !supportsTurnOff;
},
handleTogglePower: function() {
- this.callService(this.isIdle ? 'turn_on' : 'turn_off');
+ this.callService(this.isOff ? 'turn_on' : 'turn_off');
},
handlePrevious: function() {
- this.callService('media_prev_track');
+ this.callService('media_previous_track');
},
- handlePlayPause: function() {
+ handlePlaybackControl: function() {
+ if (this.isPlaying && !this.supportsPause) {
+ alert('This case is not supported yet');
+ }
this.callService('media_play_pause');
},
@@ -25933,14 +23985,16 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
},
handleVolumeTap: function() {
- this.callService('volume_mute', { mute: !this.isMuted });
+ if (!this.supportsVolumeMute) {
+ return;
+ }
+ this.callService('volume_mute', { is_volume_muted: !this.isMuted });
},
volumeSliderChanged: function(ev) {
var volPercentage = parseFloat(ev.target.value);
var vol = volPercentage > 0 ? volPercentage / 100 : 0;
-
- this.callService('volume_set', { volume: vol });
+ this.callService('volume_set', { volume_level: vol });
},
callService: function(service, data) {
@@ -25961,19 +24015,19 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
}
@media (max-width: 720px) {
:host .camera-image {
- max-width: calc(100%);
+ max-width: 100%;
height: initial
}
:host .camera-page {
- max-width: calc(100%);
- max-height: calc(100%);
+ max-width: 100%;
+ max-height: 100%;
}
}
@media (max-width: 620px) {
:host .camera-image {
- max-width: calc(100%);
+ max-width: 100%;
height: initial
}
}
@@ -25986,9 +24040,9 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
-
-
-
+
+
+
@@ -26006,46 +24060,20 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
properties: {
stateObj: {
- type: Object,
- observer: 'stateObjChanged',
+ type: Object
},
dialogOpen: {
- type: Object,
- observer: 'dialogOpenChanged',
+ type: Boolean,
},
camera_image_url: {
type: String,
}
},
- stateObjChanged: function(newVal, oldVal) {
- if (newVal) {
-
- }
-
- },
-
- computeClassNames: function(stateObj) {
- return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
- },
-
- dialogOpenChanged: function(newVal, oldVal) {
- if (newVal) {
- this.startImageStream();
- }
- else {
- this.stopImageStream();
- }
- },
-
- startImageStream: function() {
- this.camera_image_url = this.stateObj.attributes['stream_url'];
- this.isStreaming = true;
- },
-
- stopImageStream: function() {
- this.camera_image_url = this.stateObj.attributes['still_image_url'] + '?t=' + Date.now();
- this.isStreaming = false;
+ computeCameraImageUrl: function(dialogOpen) {
+ return dialogOpen ?
+ this.stateObj.attributes['stream_url'] :
+ this.stateObj.attributes['still_image_url'];
},
});
@@ -26141,7 +24169,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
-
+
@@ -26157,9 +24185,13 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -26363,63 +24858,77 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
}
-
-
+
+
-
+
+
+
+
-
+
-
-
-
-
- Home Assistant
-
+
-
-
-
-
-
+
+
-
@@ -26520,192 +25152,83 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
@@ -26719,9 +25242,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
}
-
-
+
@@ -26735,10 +25257,9 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
diff --git a/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html b/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html
index ca543ceb028..a2f2fb00d9b 100644
--- a/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html
+++ b/homeassistant/components/frontend/www_static/polymer/components/ha-logbook.html
@@ -10,6 +10,9 @@
}
+
+ No logbook entries found.
+
@@ -27,6 +30,10 @@
value: [],
},
},
+
+ noEntries: function(entries) {
+ return !entries.length;
+ }
});
})();
diff --git a/homeassistant/components/frontend/www_static/polymer/components/ha-sidebar.html b/homeassistant/components/frontend/www_static/polymer/components/ha-sidebar.html
new file mode 100644
index 00000000000..8746ee126c8
--- /dev/null
+++ b/homeassistant/components/frontend/www_static/polymer/components/ha-sidebar.html
@@ -0,0 +1,247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/homeassistant/components/frontend/www_static/polymer/components/ha-voice-command-progress.html b/homeassistant/components/frontend/www_static/polymer/components/ha-voice-command-progress.html
new file mode 100644
index 00000000000..44d29a0151c
--- /dev/null
+++ b/homeassistant/components/frontend/www_static/polymer/components/ha-voice-command-progress.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+ {{finalTranscript}}
+ [[interimTranscript]]
+
+
+
+
+
+
diff --git a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html
index 6ab4bc65b29..3575d6b5d94 100644
--- a/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html
+++ b/homeassistant/components/frontend/www_static/polymer/components/logbook-entry.html
@@ -54,14 +54,14 @@
diff --git a/homeassistant/components/frontend/www_static/polymer/home-assistant-js b/homeassistant/components/frontend/www_static/polymer/home-assistant-js
index 015edf9c28a..14f2bb779eb 160000
--- a/homeassistant/components/frontend/www_static/polymer/home-assistant-js
+++ b/homeassistant/components/frontend/www_static/polymer/home-assistant-js
@@ -1 +1 @@
-Subproject commit 015edf9c28a63122aa8f6bc153f0c0ddfaad1caa
+Subproject commit 14f2bb779eb165bce236dcdc69d83e08ab73da1c
diff --git a/homeassistant/components/frontend/www_static/polymer/home-assistant.html b/homeassistant/components/frontend/www_static/polymer/home-assistant.html
index 758ca4ca482..8c4ed824cdd 100644
--- a/homeassistant/components/frontend/www_static/polymer/home-assistant.html
+++ b/homeassistant/components/frontend/www_static/polymer/home-assistant.html
@@ -21,9 +21,8 @@
}
-
-
+
@@ -37,10 +36,9 @@
diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/login-form.html b/homeassistant/components/frontend/www_static/polymer/layouts/login-form.html
index 800bbfcedc1..43b1e97590c 100644
--- a/homeassistant/components/frontend/www_static/polymer/layouts/login-form.html
+++ b/homeassistant/components/frontend/www_static/polymer/layouts/login-form.html
@@ -13,6 +13,10 @@
-
+
diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-fire-event.html b/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-fire-event.html
index 613fd61d88c..57cc3ef5da4 100644
--- a/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-fire-event.html
+++ b/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-fire-event.html
@@ -80,7 +80,7 @@
return;
}
- eventActions.fire(this.eventType, eventData);
+ eventActions.fireEvent(this.eventType, eventData);
},
computeFormClasses: function(narrow) {
diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-set-state.html b/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-set-state.html
index 85007426d0d..6771bbdb1ba 100644
--- a/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-set-state.html
+++ b/homeassistant/components/frontend/www_static/polymer/layouts/partial-dev-set-state.html
@@ -50,8 +50,9 @@
diff --git a/homeassistant/components/frontend/www_static/polymer/managers/notification-manager.html b/homeassistant/components/frontend/www_static/polymer/managers/notification-manager.html
index 07eb7db835b..0d38e087ebd 100644
--- a/homeassistant/components/frontend/www_static/polymer/managers/notification-manager.html
+++ b/homeassistant/components/frontend/www_static/polymer/managers/notification-manager.html
@@ -15,33 +15,26 @@
diff --git a/homeassistant/components/frontend/www_static/polymer/managers/preferences-manager.html b/homeassistant/components/frontend/www_static/polymer/managers/preferences-manager.html
new file mode 100644
index 00000000000..a70f8070d69
--- /dev/null
+++ b/homeassistant/components/frontend/www_static/polymer/managers/preferences-manager.html
@@ -0,0 +1,40 @@
+
+
+
diff --git a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-configurator.html b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-configurator.html
index ea4c592ecfc..3309ff91cd9 100644
--- a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-configurator.html
+++ b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-configurator.html
@@ -52,13 +52,14 @@
diff --git a/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html b/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html
index fbfbe6d2c5a..30b71031163 100644
--- a/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html
+++ b/homeassistant/components/frontend/www_static/polymer/resources/moment-js.html
@@ -2,7 +2,7 @@
Wrapping JS in an HTML file will prevent it from being loaded twice.
-->
-
+
+
diff --git a/homeassistant/components/frontend/www_static/polymer/resources/store-listener-behavior.html b/homeassistant/components/frontend/www_static/polymer/resources/store-listener-behavior.html
index 2b9e8ece15b..7ad23230a4e 100644
--- a/homeassistant/components/frontend/www_static/polymer/resources/store-listener-behavior.html
+++ b/homeassistant/components/frontend/www_static/polymer/resources/store-listener-behavior.html
@@ -1,21 +1,42 @@
diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js
index f6acf267eba..abb6ff6700e 100644
--- a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js
+++ b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js
@@ -7,6 +7,7 @@
* 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.2
-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}(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){b.push(e)}var d=a||"scheme start",l=0,u="",_=!1,g=!1,b=[];e:for(;(e[l-1]!=f||0==l)&&!this._isInvalid;){var w=e[l];switch(d){case"scheme start":if(!w||!m.test(w)){if(a){c("Invalid scheme.");break e}u="",d="no scheme";continue}u+=w.toLowerCase(),d="scheme";break;case"scheme":if(w&&v.test(w))u+=w.toLowerCase();else{if(":"!=w){if(a){if(f==w)break e;c("Code point not allowed in scheme: "+w);break e}u="",l=0,d="no scheme";continue}if(this._scheme=u,u="",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":"?"==w?(query="?",d="query"):"#"==w?(this._fragment="#",d="fragment"):f!=w&&" "!=w&&"\n"!=w&&"\r"!=w&&(this._schemeData+=o(w));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=w||"/"!=e[l+1]){c("Expected /, got: "+w),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),f==w){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("/"==w||"\\"==w)"\\"==w&&c("\\ is an invalid code point."),d="relative slash";else if("?"==w)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("#"!=w){var y=e[l+1],E=e[l+2];("file"!=this._scheme||!m.test(w)||":"!=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("/"!=w&&"\\"!=w){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==w&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=w){c("Expected '/', got: "+w),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=w){c("Expected '/', got: "+w);continue}break;case"authority ignore slashes":if("/"!=w&&"\\"!=w){d="authority";continue}c("Expected authority, got: "+w);break;case"authority":if("@"==w){_&&(c("@ already seen."),u+="%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){w.push(e),b||(b=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){b=!1;var e=w;w=[],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 u?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 l="import",u=Boolean(l in document.createElement("link")),h=Boolean(window.ShadowDOMPolyfill),p=function(e){return h?ShadowDOMPolyfill.wrapIfNeeded(e):e},f=p(document),m={get:function(){var e=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|Edge/.test(navigator.userAgent),_=v?"complete":"interactive",g="readystatechange";u&&(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){HTMLImports.ready=!0,HTMLImports.readyTime=(new Date).getTime();var t=f.createEvent("CustomEvent");t.initCustomEvent("HTMLImportsLoaded",!0,!0,e),f.dispatchEvent(t)}),e.IMPORT_LINK_TYPE=l,e.useNative=u,e.rootDocument=f,e.whenReady=t,e.isIE=v}(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}(HTMLImports),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}),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}),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}),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}),HTMLImports.addModule(function(e){function t(e){return"link"===e.localName&&e.rel===l}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,l=e.IMPORT_LINK_TYPE,u="link[rel="+l+"]",h={documentSelectors:u,importsSelectors:[u,"link[rel=stylesheet]","style","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(HTMLImports.__importsParsingHook&&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(r){t&&t(r),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.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["import"],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["import"]?!1:!0}};e.parser=h,e.IMPORT_SELECTOR=u}),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,l=e.Observer,u=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["import"]=c}u.parseNext()},bootDocument:function(e){this.loadSubtree(e),this.observer.observe(e),u.parseNext()},loadedAll:function(){u.parseNext()}},p=new d(h.loaded.bind(h),h.loadedAll.bind(h));if(h.observer=new l,!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}),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(){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},window.CustomEvent.prototype=window.Event.prototype),n();var o=e.rootDocument;"complete"===document.readyState||"interactive"===document.readyState&&!window.attachEvent?t():document.addEventListener("DOMContentLoaded",t)}}(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.useNative=!t.register&&e.hasNative&&!window.ShadowDOMPolyfill&&(!window.HTMLImports||HTMLImports.useNative)}(CustomElements),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=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?HTMLImports.IMPORT_LINK_TYPE:"none";e.forDocumentTree=o,e.forSubtree=t}),CustomElements.addModule(function(e){function t(e){return n(e)||r(e)}function n(t){return e.upgrade(t)?!0:void s(t)}function r(e){w(e,function(e){return n(e)?!0:void 0})}function o(e){s(e),h(e)&&w(e,function(e){s(e)})}function i(e){M.push(e),L||(L=!0,setTimeout(a))}function a(){L=!1;for(var e,t=M,n=0,r=t.length;r>n&&(e=t[n]);n++)e();M=[]}function s(e){E?i(function(){c(e)}):c(e)}function c(e){e.__upgraded__&&(e.attachedCallback||e.detachedCallback)&&!e.__attached&&h(e)&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function d(e){l(e),w(e,function(e){l(e)})}function l(e){E?i(function(){u(e)}):u(e)}function u(e){e.__upgraded__&&(e.attachedCallback||e.detachedCallback)&&e.__attached&&!h(e)&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function h(e){for(var t=e,n=wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function p(e){if(e.shadowRoot&&!e.shadowRoot.__watched){b.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)v(t),t=t.olderShadowRoot}}function f(e){if(b.dom){var n=e[0];if(n&&"childList"===n.type&&n.addedNodes&&n.addedNodes){for(var r=n.addedNodes[0];r&&r!==document&&!r.host;)r=r.parentNode;var o=r&&(r.URL||r._URL||r.host&&r.host.localName)||"";o=o.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",e.length,o||"")}e.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e)}),T(e.removedNodes,function(e){e.localName&&d(e)}))}),b.dom&&console.groupEnd()}function m(e){for(e=wrap(e),e||(e=wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(t.takeRecords()),a())}function v(e){if(!e.__observer){var t=new MutationObserver(f);t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function _(e){e=wrap(e),b.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop()),t(e),v(e),b.dom&&console.groupEnd()}function g(e){y(e,_)}var b=e.flags,w=e.forSubtree,y=e.forDocumentTree,E=!window.MutationObserver||window.MutationObserver===window.JsMutationObserver;e.hasPolyfillMutations=E;var L=!1,M=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),N=Element.prototype.createShadowRoot;N&&(Element.prototype.createShadowRoot=function(){var e=N.call(this);return CustomElements.watchShadow(this),e}),e.watchShadow=p,e.upgradeDocumentTree=g,e.upgradeSubtree=r,e.upgradeAll=t,e.attachedNode=o,e.takeRecords=m}),CustomElements.addModule(function(e){function t(t){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var r=t.getAttribute("is"),o=e.getRegisteredDefinition(r||t.localName);if(o){if(r&&o.tag==t.localName)return n(t,o);if(!r&&!o["extends"])return n(t,o)}}}function n(t,n){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),e.attachedNode(t),e.upgradeSubtree(t),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}),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),l(c.__name,c),c.ctor=u(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&&w(r,HTMLElement),r)}function f(e,t){var n=e[t];e[t]=function(){var e=n.apply(this,arguments);return g(e),e}}var m,v=e.isIE11OrOlder,_=e.upgradeDocumentTree,g=e.upgradeAll,b=e.upgradeWithDefinition,w=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){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(wrap(document)),window.HTMLImports&&(HTMLImports.__importsParsingHook=function(e){a(wrap(e["import"]))}),CustomElements.ready=!0,setTimeout(function(){CustomElements.readyTime=Date.now(),window.HTMLImports&&(CustomElements.elapsed=CustomElements.readyTime-HTMLImports.readyTime),document.dispatchEvent(new CustomEvent("WebComponentsReady",{bubbles:!0}))})}var n=e.useNative,r=e.initializeModules,o=/Trident/.test(navigator.userAgent);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;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),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},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 s=window.HTMLImports&&!HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t();e.isIE11OrOlder=o}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){var e="template";HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=e.ownerDocument.createDocumentFragment());for(var t;t=e.firstChild;)e.content.appendChild(t)},HTMLTemplateElement.bootstrap=function(t){for(var n,r=t.querySelectorAll(e),o=0,i=r.length;i>o&&(n=r[o]);o++)HTMLTemplateElement.decorate(n)},addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var t=document.createElement;document.createElement=function(){"use strict";var e=t.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e}}(),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.5
+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){b.push(e)}var d=a||"scheme start",u=0,l="",_=!1,w=!1,b=[];e:for(;(e[u-1]!=f||0==u)&&!this._isInvalid;){var g=e[u];switch(d){case"scheme start":if(!g||!m.test(g)){if(a){c("Invalid scheme.");break e}l="",d="no scheme";continue}l+=g.toLowerCase(),d="scheme";break;case"scheme":if(g&&v.test(g))l+=g.toLowerCase();else{if(":"!=g){if(a){if(f==g)break e;c("Code point not allowed in scheme: "+g);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":"?"==g?(this._query="?",d="query"):"#"==g?(this._fragment="#",d="fragment"):f!=g&&" "!=g&&"\n"!=g&&"\r"!=g&&(this._schemeData+=o(g));break;case"no scheme":if(s&&t(s._scheme)){d="relative";continue}c("Missing scheme."),n.call(this);break;case"relative or authority":if("/"!=g||"/"!=e[u+1]){c("Expected /, got: "+g),d="relative";continue}d="authority ignore slashes";break;case"relative":if(this._isRelative=!0,"file"!=this._scheme&&(this._scheme=s._scheme),f==g){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("/"==g||"\\"==g)"\\"==g&&c("\\ is an invalid code point."),d="relative slash";else if("?"==g)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("#"!=g){var y=e[u+1],E=e[u+2];("file"!=this._scheme||!m.test(g)||":"!=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("/"!=g&&"\\"!=g){"file"!=this._scheme&&(this._host=s._host,this._port=s._port,this._username=s._username,this._password=s._password),d="relative path";continue}"\\"==g&&c("\\ is an invalid code point."),d="file"==this._scheme?"file host":"authority ignore slashes";break;case"authority first slash":if("/"!=g){c("Expected '/', got: "+g),d="authority ignore slashes";continue}d="authority second slash";break;case"authority second slash":if(d="authority ignore slashes","/"!=g){c("Expected '/', got: "+g);continue}break;case"authority ignore slashes":if("/"!=g&&"\\"!=g){d="authority";continue}c("Expected authority, got: "+g);break;case"authority":if("@"==g){_&&(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){g.push(e),b||(b=!0,m(r))}function n(e){return window.ShadowDOMPolyfill&&window.ShadowDOMPolyfill.wrapIfNeeded(e)||e}function r(){b=!1;var e=g;g=[],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]","style","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(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(r){t&&t(r),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.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["import"],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["import"]?!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["import"]=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.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){return n(e)||r(e)}function n(t){return e.upgrade(t)?!0:void s(t)}function r(e){g(e,function(e){return n(e)?!0:void 0})}function o(e){s(e),h(e)&&g(e,function(e){s(e)})}function i(e){M.push(e),L||(L=!0,setTimeout(a))}function a(){L=!1;for(var e,t=M,n=0,r=t.length;r>n&&(e=t[n]);n++)e();M=[]}function s(e){E?i(function(){c(e)}):c(e)}function c(e){e.__upgraded__&&(e.attachedCallback||e.detachedCallback)&&!e.__attached&&h(e)&&(e.__attached=!0,e.attachedCallback&&e.attachedCallback())}function d(e){u(e),g(e,function(e){u(e)})}function u(e){E?i(function(){l(e)}):l(e)}function l(e){e.__upgraded__&&(e.attachedCallback||e.detachedCallback)&&e.__attached&&!h(e)&&(e.__attached=!1,e.detachedCallback&&e.detachedCallback())}function h(e){for(var t=e,n=wrap(document);t;){if(t==n)return!0;t=t.parentNode||t.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&t.host}}function p(e){if(e.shadowRoot&&!e.shadowRoot.__watched){b.dom&&console.log("watching shadow-root for: ",e.localName);for(var t=e.shadowRoot;t;)v(t),t=t.olderShadowRoot}}function f(e){if(b.dom){var n=e[0];if(n&&"childList"===n.type&&n.addedNodes&&n.addedNodes){for(var r=n.addedNodes[0];r&&r!==document&&!r.host;)r=r.parentNode;var o=r&&(r.URL||r._URL||r.host&&r.host.localName)||"";o=o.split("/?").shift().split("/").pop()}console.group("mutations (%d) [%s]",e.length,o||"")}e.forEach(function(e){"childList"===e.type&&(T(e.addedNodes,function(e){e.localName&&t(e)}),T(e.removedNodes,function(e){e.localName&&d(e)}))}),b.dom&&console.groupEnd()}function m(e){for(e=window.wrap(e),e||(e=window.wrap(document));e.parentNode;)e=e.parentNode;var t=e.__observer;t&&(f(t.takeRecords()),a())}function v(e){if(!e.__observer){var t=new MutationObserver(f);t.observe(e,{childList:!0,subtree:!0}),e.__observer=t}}function _(e){e=window.wrap(e),b.dom&&console.group("upgradeDocument: ",e.baseURI.split("/").pop()),t(e),v(e),b.dom&&console.groupEnd()}function w(e){y(e,_)}var b=e.flags,g=e.forSubtree,y=e.forDocumentTree,E=!window.MutationObserver||window.MutationObserver===window.JsMutationObserver;e.hasPolyfillMutations=E;var L=!1,M=[],T=Array.prototype.forEach.call.bind(Array.prototype.forEach),N=Element.prototype.createShadowRoot;N&&(Element.prototype.createShadowRoot=function(){var e=N.call(this);return window.CustomElements.watchShadow(this),e}),e.watchShadow=p,e.upgradeDocumentTree=w,e.upgradeSubtree=r,e.upgradeAll=t,e.attachedNode=o,e.takeRecords=m}),window.CustomElements.addModule(function(e){function t(t){if(!t.__upgraded__&&t.nodeType===Node.ELEMENT_NODE){var r=t.getAttribute("is"),o=e.getRegisteredDefinition(r||t.localName);if(o){if(r&&o.tag==t.localName)return n(t,o);if(!r&&!o["extends"])return n(t,o)}}}function n(t,n){return a.upgrade&&console.group("upgrade:",t.localName),n.is&&t.setAttribute("is",n.is),r(t,n),t.__upgraded__=!0,i(t),e.attachedNode(t),e.upgradeSubtree(t),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&&g(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.isIE11OrOlder,_=e.upgradeDocumentTree,w=e.upgradeAll,b=e.upgradeWithDefinition,g=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){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.HTMLImports&&(window.HTMLImports.__importsParsingHook=function(e){a(wrap(e["import"]))}),window.CustomElements.ready=!0,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=/Trident/.test(navigator.userAgent);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;if(window.wrap||(window.ShadowDOMPolyfill?(window.wrap=window.ShadowDOMPolyfill.wrapIfNeeded,window.unwrap=window.ShadowDOMPolyfill.unwrapIfNeeded):window.wrap=window.unwrap=function(e){return e}),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 s=window.HTMLImports&&!window.HTMLImports.ready?"HTMLImportsLoaded":"DOMContentLoaded";window.addEventListener(s,t)}else t();e.isIE11OrOlder=o}(window.CustomElements),"undefined"==typeof HTMLTemplateElement&&!function(){var e="template";HTMLTemplateElement=function(){},HTMLTemplateElement.prototype=Object.create(HTMLElement.prototype),HTMLTemplateElement.decorate=function(e){e.content||(e.content=e.ownerDocument.createDocumentFragment());for(var t;t=e.firstChild;)e.content.appendChild(t)},HTMLTemplateElement.bootstrap=function(t){for(var n,r=t.querySelectorAll(e),o=0,i=r.length;i>o&&(n=r[o]);o++)HTMLTemplateElement.decorate(n)},window.addEventListener("DOMContentLoaded",function(){HTMLTemplateElement.bootstrap(document)});var t=document.createElement;document.createElement=function(){"use strict";var e=t.apply(document,arguments);return"template"==e.localName&&HTMLTemplateElement.decorate(e),e}}(),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/history.py b/homeassistant/components/history.py
index 14be60fa97e..1d2ccc9ab14 100644
--- a/homeassistant/components/history.py
+++ b/homeassistant/components/history.py
@@ -9,12 +9,16 @@ from datetime import timedelta
from itertools import groupby
from collections import defaultdict
-import homeassistant.util.dt as date_util
+import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
+from homeassistant.const import HTTP_BAD_REQUEST
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
+URL_HISTORY_PERIOD = re.compile(
+ r'/api/history/period(?:/(?P\d{4}-\d{1,2}-\d{1,2})|)')
+
def last_5_states(entity_id):
""" Return the last 5 states for entity_id. """
@@ -111,8 +115,7 @@ def setup(hass, config):
r'recent_states'),
_api_last_5_states)
- hass.http.register_path(
- 'GET', re.compile(r'/api/history/period'), _api_history_period)
+ hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
return True
@@ -128,10 +131,25 @@ def _api_last_5_states(handler, path_match, data):
def _api_history_period(handler, path_match, data):
""" Return history over a period of time. """
- # 1 day for now..
- start_time = date_util.utcnow() - timedelta(seconds=86400)
+ date_str = path_match.group('date')
+ one_day = timedelta(seconds=86400)
+
+ if date_str:
+ start_date = dt_util.date_str_to_date(date_str)
+
+ if start_date is None:
+ handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
+ return
+
+ start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
+ else:
+ start_time = dt_util.utcnow() - one_day
+
+ end_time = start_time + one_day
+
+ print("Fetchign", start_time, end_time)
entity_id = data.get('filter_entity_id')
handler.write_json(
- state_changes_during_period(start_time, entity_id=entity_id).values())
+ state_changes_during_period(start_time, end_time, entity_id).values())
diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py
index 2ff9b5caeaf..d750c3b09be 100644
--- a/homeassistant/components/isy994.py
+++ b/homeassistant/components/isy994.py
@@ -28,6 +28,7 @@ DISCOVER_SENSORS = "isy994.sensors"
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
+CONF_TLS_VER = 'tls'
# setup logger
_LOGGER = logging.getLogger(__name__)
@@ -42,7 +43,6 @@ def setup(hass, config):
import PyISY
except ImportError:
_LOGGER.error("Error while importing dependency PyISY.")
-
return False
# pylint: disable=global-statement
@@ -74,10 +74,12 @@ def setup(hass, config):
global HIDDEN_STRING
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING))
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
+ tls_version = config[DOMAIN].get(CONF_TLS_VER, None)
# connect to ISY controller
global ISY
- ISY = PyISY.ISY(addr, port, user, password, use_https=https, log=_LOGGER)
+ ISY = PyISY.ISY(addr, port, user, password, use_https=https,
+ tls_ver=tls_version, log=_LOGGER)
if not ISY.connected:
return False
diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py
index b4959b48055..b59fe8d39dc 100644
--- a/homeassistant/components/keyboard.py
+++ b/homeassistant/components/keyboard.py
@@ -8,7 +8,7 @@ import logging
from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
- SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
+ SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_PLAY_PAUSE)
@@ -43,7 +43,7 @@ def media_next_track(hass):
def media_prev_track(hass):
""" Press the keyboard button for prev track. """
- hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
+ hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK)
def setup(hass, config):
@@ -79,7 +79,7 @@ def setup(hass, config):
lambda service:
keyboard.tap_key(keyboard.media_next_track_key))
- hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK,
+ hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_prev_track_key))
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index ca887b97a19..54919a73331 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -53,6 +53,7 @@ import os
import csv
from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util
from homeassistant.const import (
@@ -87,6 +88,10 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
+# Apply an effect to the light, can be EFFECT_COLORLOOP
+ATTR_EFFECT = "effect"
+EFFECT_COLORLOOP = "colorloop"
+
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
@@ -96,6 +101,11 @@ DISCOVERY_PLATFORMS = {
discovery.services.PHILIPS_HUE: 'hue',
}
+PROP_TO_ATTR = {
+ 'brightness': ATTR_BRIGHTNESS,
+ 'color_xy': ATTR_XY_COLOR,
+}
+
_LOGGER = logging.getLogger(__name__)
@@ -108,7 +118,8 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None,
- rgb_color=None, xy_color=None, profile=None, flash=None):
+ rgb_color=None, xy_color=None, profile=None, flash=None,
+ effect=None):
""" Turns all or specified light on. """
data = {
key: value for key, value in [
@@ -119,6 +130,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_FLASH, flash),
+ (ATTR_EFFECT, effect),
] if value is not None
}
@@ -247,11 +259,16 @@ def setup(hass, config):
elif dat[ATTR_FLASH] == FLASH_LONG:
params[ATTR_FLASH] = FLASH_LONG
+ if ATTR_EFFECT in dat:
+ if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
+ params[ATTR_EFFECT] = EFFECT_COLORLOOP
+
for light in target_lights:
light.turn_on(**params)
for light in target_lights:
- light.update_ha_state(True)
+ if light.should_poll:
+ light.update_ha_state(True)
# Listen for light on and light off service calls
hass.services.register(DOMAIN, SERVICE_TURN_ON,
@@ -261,3 +278,41 @@ def setup(hass, config):
handle_light_service)
return True
+
+
+class Light(ToggleEntity):
+ """ Represents a light within Home Assistant. """
+ # pylint: disable=no-self-use
+
+ @property
+ def brightness(self):
+ """ Brightness of this light between 0..255. """
+ return None
+
+ @property
+ def color_xy(self):
+ """ XY color value [float, float]. """
+ return None
+
+ @property
+ def device_state_attributes(self):
+ """ Returns device specific state attributes. """
+ return None
+
+ @property
+ def state_attributes(self):
+ """ Returns optional state attributes. """
+ data = {}
+
+ if self.is_on:
+ for prop, attr in PROP_TO_ATTR.items():
+ value = getattr(self, prop)
+ if value:
+ data[attr] = value
+
+ device_attr = self.device_state_attributes
+
+ if device_attr is not None:
+ data.update(device_attr)
+
+ return data
diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py
index 3949c765023..5c6b1ae6165 100644
--- a/homeassistant/components/light/demo.py
+++ b/homeassistant/components/light/demo.py
@@ -7,9 +7,8 @@ Demo platform that implements lights.
"""
import random
-from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
-from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR
+from homeassistant.components.light import (
+ Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR)
LIGHT_COLORS = [
@@ -22,16 +21,16 @@ LIGHT_COLORS = [
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo lights. """
add_devices_callback([
- DemoLight("Bed Light", STATE_OFF),
- DemoLight("Ceiling", STATE_ON),
- DemoLight("Kitchen", STATE_ON)
+ DemoLight("Bed Light", False),
+ DemoLight("Ceiling", True),
+ DemoLight("Kitchen", True)
])
-class DemoLight(ToggleEntity):
+class DemoLight(Light):
""" Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180):
- self._name = name or DEVICE_DEFAULT_NAME
+ self._name = name
self._state = state
self._xy = xy or random.choice(LIGHT_COLORS)
self._brightness = brightness
@@ -47,27 +46,23 @@ class DemoLight(ToggleEntity):
return self._name
@property
- def state(self):
- """ Returns the name of the device if any. """
- return self._state
+ def brightness(self):
+ """ Brightness of this light between 0..255. """
+ return self._brightness
@property
- def state_attributes(self):
- """ Returns optional state attributes. """
- if self.is_on:
- return {
- ATTR_BRIGHTNESS: self._brightness,
- ATTR_XY_COLOR: self._xy,
- }
+ def color_xy(self):
+ """ XY color value. """
+ return self._xy
@property
def is_on(self):
""" True if device is on. """
- return self._state == STATE_ON
+ return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
- self._state = STATE_ON
+ self._state = True
if ATTR_XY_COLOR in kwargs:
self._xy = kwargs[ATTR_XY_COLOR]
@@ -75,6 +70,9 @@ class DemoLight(ToggleEntity):
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
+ self.update_ha_state()
+
def turn_off(self, **kwargs):
""" Turn the device off. """
- self._state = STATE_OFF
+ self._state = False
+ self.update_ha_state()
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 683d8a1a4c9..0b2cf1e2dd7 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -6,11 +6,11 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
-from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.const import CONF_HOST
+from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import (
- ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
- ATTR_FLASH, FLASH_LONG, FLASH_SHORT)
+ Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
+ ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT,
+ EFFECT_COLORLOOP)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@@ -131,7 +131,7 @@ def request_configuration(host, hass, add_devices_callback):
)
-class HueLight(ToggleEntity):
+class HueLight(Light):
""" Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights):
@@ -149,19 +149,17 @@ class HueLight(ToggleEntity):
@property
def name(self):
""" Get the mame of the Hue light. """
- return self.info.get('name', 'No name')
+ return self.info.get('name', DEVICE_DEFAULT_NAME)
@property
- def state_attributes(self):
- """ Returns optional state attributes. """
- attr = {}
+ def brightness(self):
+ """ Brightness of this light between 0..255. """
+ return self.info['state']['bri']
- if self.is_on:
- attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
- if 'xy' in self.info['state']:
- attr[ATTR_XY_COLOR] = self.info['state']['xy']
-
- return attr
+ @property
+ def color_xy(self):
+ """ XY color value. """
+ return self.info['state'].get('xy')
@property
def is_on(self):
@@ -194,6 +192,13 @@ class HueLight(ToggleEntity):
else:
command['alert'] = 'none'
+ effect = kwargs.get(ATTR_EFFECT)
+
+ if effect == EFFECT_COLORLOOP:
+ command['effect'] = 'colorloop'
+ else:
+ command['effect'] = 'none'
+
self.bridge.set_light(self.light_id, command)
def turn_off(self, **kwargs):
diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py
index deb98e2428e..649f642e077 100644
--- a/homeassistant/components/light/limitlessled.py
+++ b/homeassistant/components/light/limitlessled.py
@@ -23,9 +23,8 @@ light:
"""
import logging
-from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
-from homeassistant.components.light import ATTR_BRIGHTNESS
+from homeassistant.const import DEVICE_DEFAULT_NAME
+from homeassistant.components.light import Light, ATTR_BRIGHTNESS
_LOGGER = logging.getLogger(__name__)
@@ -43,18 +42,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
lights = []
for i in range(1, 5):
if 'group_%d_name' % (i) in config:
- lights.append(
- LimitlessLED(
- led,
- i,
- config['group_%d_name' % (i)]
- )
- )
+ lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
add_devices_callback(lights)
-class LimitlessLED(ToggleEntity):
+class LimitlessLED(Light):
""" Represents a LimitlessLED light """
def __init__(self, led, group, name):
@@ -65,7 +58,7 @@ class LimitlessLED(ToggleEntity):
self.led.off(self.group)
self._name = name or DEVICE_DEFAULT_NAME
- self._state = STATE_OFF
+ self._state = False
self._brightness = 100
@property
@@ -79,33 +72,26 @@ class LimitlessLED(ToggleEntity):
return self._name
@property
- def state(self):
- """ Returns the name of the device if any. """
- return self._state
-
- @property
- def state_attributes(self):
- """ Returns optional state attributes. """
- if self.is_on:
- return {
- ATTR_BRIGHTNESS: self._brightness,
- }
+ def brightness(self):
+ return self._brightness
@property
def is_on(self):
""" True if device is on. """
- return self._state == STATE_ON
+ return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
- self._state = STATE_ON
+ self._state = True
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
self.led.set_brightness(self._brightness, self.group)
+ self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
- self._state = STATE_OFF
+ self._state = False
self.led.off(self.group)
+ self.update_ha_state()
diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py
index 6d28c196326..d3c35ae0640 100644
--- a/homeassistant/components/light/tellstick.py
+++ b/homeassistant/components/light/tellstick.py
@@ -1,9 +1,8 @@
""" Support for Tellstick lights. """
import logging
# pylint: disable=no-name-in-module, import-error
-from homeassistant.components.light import ATTR_BRIGHTNESS
+from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME
-from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
@@ -27,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(lights)
-class TellstickLight(ToggleEntity):
+class TellstickLight(Light):
""" Represents a tellstick light """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF |
@@ -38,7 +37,7 @@ class TellstickLight(ToggleEntity):
def __init__(self, tellstick):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
- self.brightness = 0
+ self._brightness = 0
@property
def name(self):
@@ -48,34 +47,28 @@ class TellstickLight(ToggleEntity):
@property
def is_on(self):
""" True if switch is on. """
- return self.brightness > 0
+ return self._brightness > 0
+
+ @property
+ def brightness(self):
+ """ Brightness of this light between 0..255. """
+ return self._brightness
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
- self.brightness = 0
+ self._brightness = 0
def turn_on(self, **kwargs):
""" Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is None:
- self.brightness = 255
+ self._brightness = 255
else:
- self.brightness = brightness
+ self._brightness = brightness
- self.tellstick.dim(self.brightness)
-
- @property
- def state_attributes(self):
- """ Returns optional state attributes. """
- attr = {
- ATTR_FRIENDLY_NAME: self.name
- }
-
- attr[ATTR_BRIGHTNESS] = int(self.brightness)
-
- return attr
+ self.tellstick.dim(self._brightness)
def update(self):
""" Update state of the light. """
@@ -83,12 +76,12 @@ class TellstickLight(ToggleEntity):
self.last_sent_command_mask)
if last_command == tellcore_constants.TELLSTICK_TURNON:
- self.brightness = 255
+ self._brightness = 255
elif last_command == tellcore_constants.TELLSTICK_TURNOFF:
- self.brightness = 0
+ self._brightness = 0
elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or
last_command == tellcore_constants.TELLSTICK_DOWN):
last_sent_value = self.tellstick.last_sent_value()
if last_sent_value is not None:
- self.brightness = last_sent_value
+ self._brightness = last_sent_value
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index cad31d41cab..acc9a36e494 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -4,12 +4,14 @@ homeassistant.components.logbook
Parses events and generates a human log.
"""
+from datetime import timedelta
from itertools import groupby
+import re
from homeassistant import State, DOMAIN as HA_DOMAIN
from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
- EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
@@ -17,12 +19,10 @@ import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
-URL_LOGBOOK = '/api/logbook'
+URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P\d{4}-\d{1,2}-\d{1,2})|)')
-QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
- ORDER BY time_fired
"""
GROUP_BY_MINUTES = 15
@@ -37,11 +37,26 @@ def setup(hass, config):
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
- start_today = dt_util.now().replace(hour=0, minute=0, second=0)
+ date_str = path_match.group('date')
- handler.write_json(humanify(
- recorder.query_events(
- QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),))))
+ if date_str:
+ start_date = dt_util.date_str_to_date(date_str)
+
+ if start_date is None:
+ handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
+ return
+
+ start_day = dt_util.start_of_local_day(start_date)
+ else:
+ start_day = dt_util.start_of_local_day()
+
+ end_day = start_day + timedelta(days=1)
+
+ events = recorder.query_events(
+ QUERY_EVENTS_BETWEEN,
+ (dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
+
+ handler.write_json(humanify(events))
class Entry(object):
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 8a080e828da..0cb4608ba1b 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -10,11 +10,12 @@ from homeassistant.components import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
- ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
+ STATE_OFF, STATE_UNKNOWN, STATE_PLAYING,
+ ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
- SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
+ SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
DOMAIN = 'media_player'
DEPENDENCIES = []
@@ -28,28 +29,70 @@ DISCOVERY_PLATFORMS = {
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
-STATE_NO_APP = 'idle'
-
-ATTR_STATE = 'state'
-ATTR_OPTIONS = 'options'
-ATTR_MEDIA_STATE = 'media_state'
+ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
+ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
+ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
+ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
+ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist'
-ATTR_MEDIA_ALBUM = 'media_album'
-ATTR_MEDIA_IMAGE_URL = 'media_image_url'
-ATTR_MEDIA_VOLUME = 'media_volume'
-ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted'
-ATTR_MEDIA_DURATION = 'media_duration'
-ATTR_MEDIA_DATE = 'media_date'
+ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
+ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
+ATTR_MEDIA_TRACK = 'media_track'
+ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
+ATTR_MEDIA_SEASON = 'media_season'
+ATTR_MEDIA_EPISODE = 'media_episode'
+ATTR_APP_ID = 'app_id'
+ATTR_APP_NAME = 'app_name'
+ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
-MEDIA_STATE_UNKNOWN = 'unknown'
-MEDIA_STATE_PLAYING = 'playing'
-MEDIA_STATE_PAUSED = 'paused'
-MEDIA_STATE_STOPPED = 'stopped'
+MEDIA_TYPE_MUSIC = 'music'
+MEDIA_TYPE_TVSHOW = 'tvshow'
+MEDIA_TYPE_VIDEO = 'movie'
+SUPPORT_PAUSE = 1
+SUPPORT_SEEK = 2
+SUPPORT_VOLUME_SET = 4
+SUPPORT_VOLUME_MUTE = 8
+SUPPORT_PREVIOUS_TRACK = 16
+SUPPORT_NEXT_TRACK = 32
+SUPPORT_YOUTUBE = 64
+SUPPORT_TURN_ON = 128
+SUPPORT_TURN_OFF = 256
-YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg'
+YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
+
+SERVICE_TO_METHOD = {
+ SERVICE_TURN_ON: 'turn_on',
+ SERVICE_TURN_OFF: 'turn_off',
+ SERVICE_VOLUME_UP: 'volume_up',
+ SERVICE_VOLUME_DOWN: 'volume_down',
+ SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
+ SERVICE_MEDIA_PLAY: 'media_play',
+ SERVICE_MEDIA_PAUSE: 'media_pause',
+ SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
+ SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
+}
+
+ATTR_TO_PROPERTY = [
+ ATTR_MEDIA_VOLUME_LEVEL,
+ ATTR_MEDIA_VOLUME_MUTED,
+ ATTR_MEDIA_CONTENT_ID,
+ ATTR_MEDIA_CONTENT_TYPE,
+ ATTR_MEDIA_DURATION,
+ ATTR_MEDIA_TITLE,
+ ATTR_MEDIA_ARTIST,
+ ATTR_MEDIA_ALBUM_NAME,
+ ATTR_MEDIA_ALBUM_ARTIST,
+ ATTR_MEDIA_TRACK,
+ ATTR_MEDIA_SERIES_TITLE,
+ ATTR_MEDIA_SEASON,
+ ATTR_MEDIA_EPISODE,
+ ATTR_APP_ID,
+ ATTR_APP_NAME,
+ ATTR_SUPPORTED_MEDIA_COMMANDS,
+]
def is_on(hass, entity_id=None):
@@ -58,7 +101,7 @@ def is_on(hass, entity_id=None):
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
- return any(not hass.states.is_state(entity_id, STATE_NO_APP)
+ return any(not hass.states.is_state(entity_id, STATE_OFF)
for entity_id in entity_ids)
@@ -90,21 +133,22 @@ def volume_down(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
-def volume_mute(hass, entity_id=None):
- """ Send the media player the command to toggle its mute state. """
- data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
+def mute_volume(hass, mute, entity_id=None):
+ """ Send the media player the command for volume down. """
+ data = {ATTR_MEDIA_VOLUME_MUTED: mute}
+
+ if entity_id:
+ data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data)
-def volume_set(hass, entity_id=None, volume=None):
- """ Set volume on media player. """
- data = {
- key: value for key, value in [
- (ATTR_ENTITY_ID, entity_id),
- (ATTR_MEDIA_VOLUME, volume),
- ] if value is not None
- }
+def set_volume_level(hass, volume, entity_id=None):
+ """ Send the media player the command for volume down. """
+ data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
+
+ if entity_id:
+ data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data)
@@ -137,24 +181,11 @@ def media_next_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
-def media_prev_track(hass, entity_id=None):
+def media_previous_track(hass, entity_id=None):
""" Send the media player the command for prev track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
-
-
-SERVICE_TO_METHOD = {
- SERVICE_TURN_ON: 'turn_on',
- SERVICE_TURN_OFF: 'turn_off',
- SERVICE_VOLUME_UP: 'volume_up',
- SERVICE_VOLUME_DOWN: 'volume_down',
- SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
- SERVICE_MEDIA_PLAY: 'media_play',
- SERVICE_MEDIA_PAUSE: 'media_pause',
- SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
- SERVICE_MEDIA_PREV_TRACK: 'media_prev_track',
-}
+ hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
def setup(hass, config):
@@ -180,35 +211,56 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler)
- def volume_set_service(service, volume):
+ def volume_set_service(service):
""" Set specified volume on the media player. """
target_players = component.extract_from_service(service)
+ if ATTR_MEDIA_VOLUME_LEVEL not in service.data:
+ return
+
+ volume = service.data[ATTR_MEDIA_VOLUME_LEVEL]
+
for player in target_players:
- player.volume_set(volume)
+ player.set_volume_level(volume)
if player.should_poll:
player.update_ha_state(True)
- hass.services.register(DOMAIN, SERVICE_VOLUME_SET,
- lambda service:
- volume_set_service(
- service, service.data.get('volume')))
+ hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
- def volume_mute_service(service, mute):
+ def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """
target_players = component.extract_from_service(service)
+ if ATTR_MEDIA_VOLUME_MUTED not in service.data:
+ return
+
+ mute = service.data[ATTR_MEDIA_VOLUME_MUTED]
+
for player in target_players:
- player.volume_mute(mute)
+ player.mute_volume(mute)
if player.should_poll:
player.update_ha_state(True)
- hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
- lambda service:
- volume_mute_service(
- service, service.data.get('mute')))
+ hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
+
+ def media_seek_service(service):
+ """ Seek to a position. """
+ target_players = component.extract_from_service(service)
+
+ if ATTR_MEDIA_SEEK_POSITION not in service.data:
+ return
+
+ position = service.data[ATTR_MEDIA_SEEK_POSITION]
+
+ for player in target_players:
+ player.seek(position)
+
+ if player.should_poll:
+ player.update_ha_state(True)
+
+ hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
def play_youtube_video_service(service, media_id):
""" Plays specified media_id on the media player. """
@@ -239,51 +291,217 @@ def setup(hass, config):
class MediaPlayerDevice(Entity):
""" ABC for media player devices. """
+ # pylint: disable=too-many-public-methods,no-self-use
+
+ # Implement these for your media player
+
+ @property
+ def state(self):
+ """ State of the player. """
+ return STATE_UNKNOWN
+
+ @property
+ def volume_level(self):
+ """ Volume level of the media player (0..1). """
+ return None
+
+ @property
+ def is_volume_muted(self):
+ """ Boolean if volume is currently muted. """
+ return None
+
+ @property
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ return None
+
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ return None
+
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ return None
+
+ @property
+ def media_image_url(self):
+ """ Image url of current playing media. """
+ return None
+
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ return None
+
+ @property
+ def media_artist(self):
+ """ Artist of current playing media. (Music track only) """
+ return None
+
+ @property
+ def media_album_name(self):
+ """ Album name of current playing media. (Music track only) """
+ return None
+
+ @property
+ def media_album_artist(self):
+ """ Album arist of current playing media. (Music track only) """
+ return None
+
+ @property
+ def media_track(self):
+ """ Track number of current playing media. (Music track only) """
+ return None
+
+ @property
+ def media_series_title(self):
+ """ Series title of current playing media. (TV Show only)"""
+ return None
+
+ @property
+ def media_season(self):
+ """ Season of current playing media. (TV Show only) """
+ return None
+
+ @property
+ def media_episode(self):
+ """ Episode of current playing media. (TV Show only) """
+ return None
+
+ @property
+ def app_id(self):
+ """ ID of the current running app. """
+ return None
+
+ @property
+ def app_name(self):
+ """ Name of the current running app. """
+ return None
+
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ return 0
+
+ @property
+ def device_state_attributes(self):
+ """ Extra attributes a device wants to expose. """
+ return None
def turn_on(self):
- """ turn media player on. """
- pass
+ """ turn the media player on. """
+ raise NotImplementedError()
def turn_off(self):
- """ turn media player off. """
- pass
+ """ turn the media player off. """
+ raise NotImplementedError()
- def volume_up(self):
- """ volume_up media player. """
- pass
+ def mute_volume(self, mute):
+ """ mute the volume. """
+ raise NotImplementedError()
- def volume_down(self):
- """ volume_down media player. """
- pass
-
- def volume_mute(self, mute):
- """ mute (true) or unmute (false) media player. """
- pass
-
- def volume_set(self, volume):
- """ set volume level of media player. """
- pass
-
- def media_play_pause(self):
- """ media_play_pause media player. """
- pass
+ def set_volume_level(self, volume):
+ """ set volume level, range 0..1. """
+ raise NotImplementedError()
def media_play(self):
- """ media_play media player. """
- pass
+ """ Send play commmand. """
+ raise NotImplementedError()
def media_pause(self):
- """ media_pause media player. """
- pass
+ """ Send pause command. """
+ raise NotImplementedError()
- def media_prev_track(self):
- """ media_prev_track media player. """
- pass
+ def media_previous_track(self):
+ """ Send previous track command. """
+ raise NotImplementedError()
def media_next_track(self):
- """ media_next_track media player. """
- pass
+ """ Send next track command. """
+ raise NotImplementedError()
+
+ def media_seek(self, position):
+ """ Send seek command. """
+ raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
- pass
+ raise NotImplementedError()
+
+ # No need to overwrite these.
+ @property
+ def support_pause(self):
+ """ Boolean if pause is supported. """
+ return bool(self.supported_media_commands & SUPPORT_PAUSE)
+
+ @property
+ def support_seek(self):
+ """ Boolean if seek is supported. """
+ return bool(self.supported_media_commands & SUPPORT_SEEK)
+
+ @property
+ def support_volume_set(self):
+ """ Boolean if setting volume is supported. """
+ return bool(self.supported_media_commands & SUPPORT_VOLUME_SET)
+
+ @property
+ def support_volume_mute(self):
+ """ Boolean if muting volume is supported. """
+ return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE)
+
+ @property
+ def support_previous_track(self):
+ """ Boolean if previous track command supported. """
+ return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK)
+
+ @property
+ def support_next_track(self):
+ """ Boolean if next track command supported. """
+ return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
+
+ @property
+ def support_youtube(self):
+ """ Boolean if YouTube is supported. """
+ return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
+
+ def volume_up(self):
+ """ volume_up media player. """
+ if self.volume_level < 1:
+ self.set_volume_level(min(1, self.volume_level + .1))
+
+ def volume_down(self):
+ """ volume_down media player. """
+ if self.volume_level > 0:
+ self.set_volume_level(max(0, self.volume_level - .1))
+
+ def media_play_pause(self):
+ """ media_play_pause media player. """
+ if self.state == STATE_PLAYING:
+ self.media_pause()
+ else:
+ self.media_play()
+
+ @property
+ def state_attributes(self):
+ """ Return the state attributes. """
+ if self.state == STATE_OFF:
+ state_attr = {
+ ATTR_SUPPORTED_MEDIA_COMMANDS: self.supported_media_commands,
+ }
+ else:
+ state_attr = {
+ attr: getattr(self, attr) for attr
+ in ATTR_TO_PROPERTY if getattr(self, attr)
+ }
+
+ if self.media_image_url:
+ state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
+
+ device_attr = self.device_state_attributes
+
+ if device_attr:
+ state_attr.update(device_attr)
+
+ return state_attr
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index d0fe80523b1..77cdf79a112 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -14,18 +14,21 @@ try:
except ImportError:
pychromecast = None
-from homeassistant.const import ATTR_ENTITY_PICTURE
+from homeassistant.const import (
+ STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
+ STATE_UNKNOWN)
-# ATTR_MEDIA_ALBUM, ATTR_MEDIA_IMAGE_URL,
-# ATTR_MEDIA_ARTIST,
from homeassistant.components.media_player import (
- MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, ATTR_MEDIA_TITLE,
- ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_DURATION,
- ATTR_MEDIA_VOLUME, ATTR_MEDIA_IS_VOLUME_MUTED,
- MEDIA_STATE_PLAYING, MEDIA_STATE_PAUSED, MEDIA_STATE_STOPPED,
- MEDIA_STATE_UNKNOWN)
+ MediaPlayerDevice,
+ SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
+ SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
+ SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
+ MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
+SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
+ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
+ SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
# pylint: disable=unused-argument
@@ -61,6 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CastDevice(MediaPlayerDevice):
""" Represents a Cast device on the network. """
+ # pylint: disable=too-many-public-methods
+
def __init__(self, host):
self.cast = pychromecast.Chromecast(host)
self.youtube = youtube.YouTubeController()
@@ -73,6 +78,8 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status
+ # Entity properties and methods
+
@property
def should_poll(self):
return False
@@ -82,57 +89,121 @@ class CastDevice(MediaPlayerDevice):
""" Returns the name of the device. """
return self.cast.device.friendly_name
+ # MediaPlayerDevice properties and methods
+
@property
def state(self):
- """ Returns the state of the device. """
- if self.cast.is_idle:
- return STATE_NO_APP
+ """ State of the player. """
+ if self.media_status is None:
+ return STATE_UNKNOWN
+ elif self.media_status.player_is_playing:
+ return STATE_PLAYING
+ elif self.media_status.player_is_paused:
+ return STATE_PAUSED
+ elif self.media_status.player_is_idle:
+ return STATE_IDLE
+ elif self.cast.is_idle:
+ return STATE_OFF
else:
- return self.cast.app_display_name
+ return STATE_UNKNOWN
@property
- def media_state(self):
- """ Returns the media state. """
- media_controller = self.cast.media_controller
-
- if media_controller.is_playing:
- return MEDIA_STATE_PLAYING
- elif media_controller.is_paused:
- return MEDIA_STATE_PAUSED
- elif media_controller.is_idle:
- return MEDIA_STATE_STOPPED
- else:
- return MEDIA_STATE_UNKNOWN
+ def volume_level(self):
+ """ Volume level of the media player (0..1). """
+ return self.cast_status.volume_level if self.cast_status else None
@property
- def state_attributes(self):
- """ Returns the state attributes. """
- cast_status = self.cast_status
- media_status = self.media_status
- media_controller = self.cast.media_controller
+ def is_volume_muted(self):
+ """ Boolean if volume is currently muted. """
+ return self.cast_status.volume_muted if self.cast_status else None
- state_attr = {
- ATTR_MEDIA_STATE: self.media_state,
- 'application_id': self.cast.app_id,
- }
+ @property
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ return self.media_status.content_id if self.media_status else None
- if cast_status:
- state_attr[ATTR_MEDIA_VOLUME] = cast_status.volume_level
- state_attr[ATTR_MEDIA_IS_VOLUME_MUTED] = cast_status.volume_muted
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ if self.media_status is None:
+ return None
+ elif self.media_status.media_is_tvshow:
+ return MEDIA_TYPE_TVSHOW
+ elif self.media_status.media_is_movie:
+ return MEDIA_TYPE_VIDEO
+ elif self.media_status.media_is_musictrack:
+ return MEDIA_TYPE_MUSIC
+ return None
- if media_status.content_id:
- state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ return self.media_status.duration if self.media_status else None
- if media_status.duration:
- state_attr[ATTR_MEDIA_DURATION] = media_status.duration
+ @property
+ def media_image_url(self):
+ """ Image url of current playing media. """
+ if self.media_status is None:
+ return None
- if media_controller.title:
- state_attr[ATTR_MEDIA_TITLE] = media_controller.title
+ images = self.media_status.images
- if media_controller.thumbnail:
- state_attr[ATTR_ENTITY_PICTURE] = media_controller.thumbnail
+ return images[0].url if images else None
- return state_attr
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ return self.media_status.title if self.media_status else None
+
+ @property
+ def media_artist(self):
+ """ Artist of current playing media. (Music track only) """
+ return self.media_status.artist if self.media_status else None
+
+ @property
+ def media_album(self):
+ """ Album of current playing media. (Music track only) """
+ return self.media_status.album_name if self.media_status else None
+
+ @property
+ def media_album_artist(self):
+ """ Album arist of current playing media. (Music track only) """
+ return self.media_status.album_artist if self.media_status else None
+
+ @property
+ def media_track(self):
+ """ Track number of current playing media. (Music track only) """
+ return self.media_status.track if self.media_status else None
+
+ @property
+ def media_series_title(self):
+ """ Series title of current playing media. (TV Show only)"""
+ return self.media_status.series_title if self.media_status else None
+
+ @property
+ def media_season(self):
+ """ Season of current playing media. (TV Show only) """
+ return self.media_status.season if self.media_status else None
+
+ @property
+ def media_episode(self):
+ """ Episode of current playing media. (TV Show only) """
+ return self.media_status.episode if self.media_status else None
+
+ @property
+ def app_id(self):
+ """ ID of the current running app. """
+ return self.cast.app_id
+
+ @property
+ def app_name(self):
+ """ Name of the current running app. """
+ return self.cast.app_display_name
+
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ return SUPPORT_CAST
def turn_on(self):
""" Turns on the ChromeCast. """
@@ -145,57 +216,42 @@ class CastDevice(MediaPlayerDevice):
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
def turn_off(self):
- """ Service to exit any running app on the specimedia player ChromeCast and
- shows idle screen. Will quit all ChromeCasts if nothing specified.
- """
+ """ Turns Chromecast off. """
self.cast.quit_app()
- def volume_up(self):
- """ Service to send the chromecast the command for volume up. """
- self.cast.volume_up()
-
- def volume_down(self):
- """ Service to send the chromecast the command for volume down. """
- self.cast.volume_down()
-
- def volume_mute(self, mute):
- """ Set media player to mute volume. """
+ def mute_volume(self, mute):
+ """ mute the volume. """
self.cast.set_volume_muted(mute)
- def volume_set(self, volume):
- """ Set media player volume, range of volume 0..1 """
+ def set_volume_level(self, volume):
+ """ set volume level, range 0..1. """
self.cast.set_volume(volume)
- def media_play_pause(self):
- """ Service to send the chromecast the command for play/pause. """
- media_state = self.media_state
-
- if media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
- self.cast.media_controller.play()
- elif media_state == MEDIA_STATE_PLAYING:
- self.cast.media_controller.pause()
-
def media_play(self):
- """ Service to send the chromecast the command for play/pause. """
- if self.media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
- self.cast.media_controller.play()
+ """ Send play commmand. """
+ self.cast.media_controller.play()
def media_pause(self):
- """ Service to send the chromecast the command for play/pause. """
- if self.media_state == MEDIA_STATE_PLAYING:
- self.cast.media_controller.pause()
+ """ Send pause command. """
+ self.cast.media_controller.pause()
- def media_prev_track(self):
- """ media_prev_track media player. """
+ def media_previous_track(self):
+ """ Send previous track command. """
self.cast.media_controller.rewind()
def media_next_track(self):
- """ media_next_track media player. """
+ """ Send next track command. """
self.cast.media_controller.skip()
- def play_youtube_video(self, video_id):
- """ Plays specified video_id on the Chromecast's YouTube channel. """
- self.youtube.play_video(video_id)
+ def media_seek(self, position):
+ """ Seek the media to a specific location. """
+ self.cast.media_controller.seek(position)
+
+ def play_youtube(self, media_id):
+ """ Plays a YouTube media. """
+ self.youtube.play_video(media_id)
+
+ # implementation of chromecast status_listener methods
def new_cast_status(self, status):
""" Called when a new cast status is received. """
diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py
index fdc17594b14..24f1e91c3ec 100644
--- a/homeassistant/components/media_player/demo.py
+++ b/homeassistant/components/media_player/demo.py
@@ -5,121 +5,334 @@ homeassistant.components.media_player.demo
Demo implementation of the media player.
"""
+from homeassistant.const import (
+ STATE_PLAYING, STATE_PAUSED, STATE_OFF)
+
from homeassistant.components.media_player import (
- MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
- ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION,
- ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED,
- YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED)
-from homeassistant.const import ATTR_ENTITY_PICTURE
+ MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
+ MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
+ SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
+ SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_NEXT_TRACK)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
add_devices([
- DemoMediaPlayer(
+ DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'),
- DemoMediaPlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours')
+ DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'),
+ DemoMusicPlayer(), DemoTVShowPlayer(),
])
-class DemoMediaPlayer(MediaPlayerDevice):
- """ A Demo media player that only supports YouTube. """
+YOUTUBE_PLAYER_SUPPORT = \
+ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
+ SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
- def __init__(self, name, youtube_id=None, media_title=None):
+MUSIC_PLAYER_SUPPORT = \
+ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
+ SUPPORT_TURN_ON | SUPPORT_TURN_OFF
+
+NETFLIX_PLAYER_SUPPORT = \
+ SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
+
+
+class AbstractDemoPlayer(MediaPlayerDevice):
+ """ Base class for demo media players. """
+ # We only implement the methods that we support
+ # pylint: disable=abstract-method
+
+ def __init__(self, name):
self._name = name
- self.is_playing = youtube_id is not None
- self.youtube_id = youtube_id
- self.media_title = media_title
- self.volume = 1.0
- self.is_volume_muted = False
+ self._player_state = STATE_PLAYING
+ self._volume_level = 1.0
+ self._volume_muted = False
@property
def should_poll(self):
- """ No polling needed for a demo componentn. """
+ """ We will push an update after each command. """
return False
@property
def name(self):
- """ Returns the name of the device. """
+ """ Name of the media player. """
return self._name
@property
def state(self):
- """ Returns the state of the device. """
- return STATE_NO_APP if self.youtube_id is None else "YouTube"
+ """ State of the player. """
+ return self._player_state
@property
- def state_attributes(self):
- """ Returns the state attributes. """
- if self.youtube_id is None:
- return
+ def volume_level(self):
+ """ Volume level of the media player (0..1). """
+ return self._volume_level
- state_attr = {
- ATTR_MEDIA_CONTENT_ID: self.youtube_id,
- ATTR_MEDIA_TITLE: self.media_title,
- ATTR_MEDIA_DURATION: 100,
- ATTR_MEDIA_VOLUME: self.volume,
- ATTR_MEDIA_IS_VOLUME_MUTED: self.is_volume_muted,
- ATTR_ENTITY_PICTURE:
- YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
- }
-
- if self.is_playing:
- state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
- else:
- state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
-
- return state_attr
+ @property
+ def is_volume_muted(self):
+ """ Boolean if volume is currently muted. """
+ return self._volume_muted
def turn_on(self):
- """ turn_off media player. """
- self.youtube_id = "eyU3bRy2x44"
- self.is_playing = False
+ """ turn the media player on. """
+ self._player_state = STATE_PLAYING
self.update_ha_state()
def turn_off(self):
- """ turn_off media player. """
- self.youtube_id = None
- self.is_playing = False
+ """ turn the media player off. """
+ self._player_state = STATE_OFF
self.update_ha_state()
- def volume_up(self):
- """ volume_up media player. """
- if self.volume < 1:
- self.volume += 0.1
- self.update_ha_state()
-
- def volume_down(self):
- """ volume_down media player. """
- if self.volume > 0:
- self.volume -= 0.1
- self.update_ha_state()
-
- def volume_mute(self, mute):
- """ mute (true) or unmute (false) media player. """
- self.is_volume_muted = mute
+ def mute_volume(self, mute):
+ """ mute the volume. """
+ self._volume_muted = mute
self.update_ha_state()
- def media_play_pause(self):
- """ media_play_pause media player. """
- self.is_playing = not self.is_playing
+ def set_volume_level(self, volume):
+ """ set volume level, range 0..1. """
+ self._volume_level = volume
self.update_ha_state()
def media_play(self):
- """ media_play media player. """
- self.is_playing = True
+ """ Send play commmand. """
+ self._player_state = STATE_PLAYING
self.update_ha_state()
def media_pause(self):
- """ media_pause media player. """
- self.is_playing = False
+ """ Send pause command. """
+ self._player_state = STATE_PAUSED
self.update_ha_state()
+
+class DemoYoutubePlayer(AbstractDemoPlayer):
+ """ A Demo media player that only supports YouTube. """
+ # We only implement the methods that we support
+ # pylint: disable=abstract-method
+
+ def __init__(self, name, youtube_id=None, media_title=None):
+ super().__init__(name)
+ self.youtube_id = youtube_id
+ self._media_title = media_title
+
+ @property
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ return self.youtube_id
+
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ return MEDIA_TYPE_VIDEO
+
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ return 360
+
+ @property
+ def media_image_url(self):
+ """ Image url of current playing media. """
+ return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
+
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ return self._media_title
+
+ @property
+ def app_name(self):
+ """ Current running app. """
+ return "YouTube"
+
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ return YOUTUBE_PLAYER_SUPPORT
+
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube_id = media_id
- self.media_title = 'Demo media title'
- self.is_playing = True
+ self._media_title = 'some YouTube video'
self.update_ha_state()
+
+
+class DemoMusicPlayer(AbstractDemoPlayer):
+ """ A Demo media player that only supports YouTube. """
+ # We only implement the methods that we support
+ # pylint: disable=abstract-method
+
+ tracks = [
+ ('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'),
+ ('Paul Elstak', 'Luv U More'),
+ ('Dune', 'Hardcore Vibes'),
+ ('Nakatomi', 'Children Of The Night'),
+ ('Party Animals',
+ 'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'),
+ ('Rob G.*', 'Ecstasy, You Got What I Need'),
+ ('Lipstick', "I'm A Raver"),
+ ('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'),
+ ('Prophet', "The Big Boys Don't Cry"),
+ ('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'),
+ ('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'),
+ ('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'),
+ ('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'),
+ ('Diss Reaction', 'Jiiieehaaaa '),
+ ('Flamman And Abraxas', 'Good To Go (Radio Mix)'),
+ ('Critical Mass', 'Dancing Together'),
+ ('Charly Lownoise & Mental Theo',
+ 'Ultimate Sex Track (Bass-D & King Matthew Remix)'),
+ ]
+
+ def __init__(self):
+ super().__init__('Walkman')
+ self._cur_track = 0
+
+ @property
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ return 'bounzz-1'
+
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ return MEDIA_TYPE_MUSIC
+
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ return 213
+
+ @property
+ def media_image_url(self):
+ """ Image url of current playing media. """
+ return 'https://graph.facebook.com/107771475912710/picture'
+
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ return self.tracks[self._cur_track][1]
+
+ @property
+ def media_artist(self):
+ """ Artist of current playing media. (Music track only) """
+ return self.tracks[self._cur_track][0]
+
+ @property
+ def media_album_name(self):
+ """ Album of current playing media. (Music track only) """
+ # pylint: disable=no-self-use
+ return "Bounzz"
+
+ @property
+ def media_track(self):
+ """ Track number of current playing media. (Music track only) """
+ return self._cur_track + 1
+
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ support = MUSIC_PLAYER_SUPPORT
+
+ if self._cur_track > 1:
+ support |= SUPPORT_PREVIOUS_TRACK
+
+ if self._cur_track < len(self.tracks)-1:
+ support |= SUPPORT_NEXT_TRACK
+
+ return support
+
+ def media_previous_track(self):
+ """ Send previous track command. """
+ if self._cur_track > 0:
+ self._cur_track -= 1
+ self.update_ha_state()
+
+ def media_next_track(self):
+ """ Send next track command. """
+ if self._cur_track < len(self.tracks)-1:
+ self._cur_track += 1
+ self.update_ha_state()
+
+
+class DemoTVShowPlayer(AbstractDemoPlayer):
+ """ A Demo media player that only supports YouTube. """
+ # We only implement the methods that we support
+ # pylint: disable=abstract-method
+
+ def __init__(self):
+ super().__init__('Lounge room')
+ self._cur_episode = 1
+ self._episode_count = 13
+
+ @property
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ return 'house-of-cards-1'
+
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ return MEDIA_TYPE_TVSHOW
+
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ return 3600
+
+ @property
+ def media_image_url(self):
+ """ Image url of current playing media. """
+ return 'https://graph.facebook.com/HouseofCards/picture'
+
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ return 'Chapter {}'.format(self._cur_episode)
+
+ @property
+ def media_series_title(self):
+ """ Series title of current playing media. (TV Show only)"""
+ return 'House of Cards'
+
+ @property
+ def media_season(self):
+ """ Season of current playing media. (TV Show only) """
+ return 1
+
+ @property
+ def media_episode(self):
+ """ Episode of current playing media. (TV Show only) """
+ return self._cur_episode
+
+ @property
+ def app_name(self):
+ """ Current running app. """
+ return "Netflix"
+
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ support = NETFLIX_PLAYER_SUPPORT
+
+ if self._cur_episode > 1:
+ support |= SUPPORT_PREVIOUS_TRACK
+
+ if self._cur_episode < self._episode_count:
+ support |= SUPPORT_NEXT_TRACK
+
+ return support
+
+ def media_previous_track(self):
+ """ Send previous track command. """
+ if self._cur_episode > 1:
+ self._cur_episode -= 1
+ self.update_ha_state()
+
+ def media_next_track(self):
+ """ Send next track command. """
+ if self._cur_episode < self._episode_count:
+ self._cur_episode += 1
+ self.update_ha_state()
diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py
new file mode 100644
index 00000000000..d31e71251dc
--- /dev/null
+++ b/homeassistant/components/media_player/kodi.py
@@ -0,0 +1,308 @@
+"""
+homeassistant.components.media_player.kodi
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Provides an interface to the XBMC/Kodi JSON-RPC API
+
+Configuration:
+
+To use Kodi add something like this to your configuration:
+
+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
+"""
+
+import urllib
+import logging
+
+from homeassistant.components.media_player import (
+ MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK)
+from homeassistant.const import (
+ STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF)
+
+try:
+ import jsonrpc_requests
+except ImportError:
+ jsonrpc_requests = None
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
+
+
+# pylint: disable=unused-argument
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """ Sets up the kodi platform. """
+
+ if jsonrpc_requests is None:
+ _LOGGER.exception(
+ "Unable to import jsonrpc_requests. "
+ "Did you maybe not install the 'jsonrpc-requests' pip module?")
+
+ return False
+
+ add_devices([
+ KodiDevice(
+ config.get('name', 'Kodi'),
+ config.get('url'),
+ auth=(
+ config.get('user', ''),
+ config.get('password', ''))),
+ ])
+
+
+def _get_image_url(kodi_url):
+ """ Helper function that parses the thumbnail URLs used by Kodi """
+ url_components = urllib.parse.urlparse(kodi_url)
+
+ if url_components.scheme == 'image':
+ return urllib.parse.unquote(url_components.netloc)
+
+
+class KodiDevice(MediaPlayerDevice):
+ """ Represents a XBMC/Kodi device. """
+
+ # pylint: disable=too-many-public-methods
+
+ def __init__(self, name, url, auth=None):
+ self._name = name
+ self._url = url
+ self._server = jsonrpc_requests.Server(url, auth=auth)
+ self._players = None
+ self._properties = None
+ self._item = None
+ self._app_properties = None
+
+ self.update()
+
+ @property
+ def name(self):
+ """ Returns the name of the device. """
+ return self._name
+
+ def _get_players(self):
+ """ Returns the active player objects or None """
+ try:
+ return self._server.Player.GetActivePlayers()
+ except jsonrpc_requests.jsonrpc.TransportError:
+ return None
+
+ @property
+ def state(self):
+ """ Returns the state of the device. """
+ if self._players is None:
+ return STATE_OFF
+
+ if len(self._players) == 0:
+ return STATE_IDLE
+
+ if self._properties['speed'] == 0:
+ return STATE_PAUSED
+ else:
+ return STATE_PLAYING
+
+ def update(self):
+ """ Retrieve latest state. """
+ self._players = self._get_players()
+
+ if self._players is not None and len(self._players) > 0:
+ player_id = self._players[0]['playerid']
+
+ assert isinstance(player_id, int)
+
+ self._properties = self._server.Player.GetProperties(
+ player_id,
+ ['time', 'totaltime', 'speed']
+ )
+
+ self._item = self._server.Player.GetItem(
+ player_id,
+ ['title', 'file', 'uniqueid', 'thumbnail', 'artist']
+ )['item']
+
+ self._app_properties = self._server.Application.GetProperties(
+ ['volume', 'muted']
+ )
+ else:
+ self._properties = None
+ self._item = None
+ self._app_properties = None
+
+ @property
+ def volume_level(self):
+ """ Volume level of the media player (0..1). """
+ if self._app_properties is not None:
+ return self._app_properties['volume'] / 100.0
+
+ @property
+ def is_volume_muted(self):
+ """ Boolean if volume is currently muted. """
+ if self._app_properties is not None:
+ return self._app_properties['muted']
+
+ @property
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ if self._item is not None:
+ return self._item['uniqueid']
+
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ if self._players is not None and len(self._players) > 0:
+ return self._players[0]['type']
+
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ if self._properties is not None:
+ total_time = self._properties['totaltime']
+
+ return (
+ total_time['hours'] * 3600 +
+ total_time['minutes'] * 60 +
+ total_time['seconds'])
+
+ @property
+ def media_image_url(self):
+ """ Image url of current playing media. """
+ if self._item is not None:
+ return _get_image_url(self._item['thumbnail'])
+
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ # find a string we can use as a title
+ if self._item is not None:
+ return self._item.get(
+ 'title',
+ self._item.get(
+ 'label',
+ self._item.get(
+ 'file',
+ 'unknown')))
+
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ return SUPPORT_KODI
+
+ def turn_off(self):
+ """ turn_off media player. """
+ self._server.System.Shutdown()
+ self.update_ha_state()
+
+ def volume_up(self):
+ """ volume_up media player. """
+ assert self._server.Input.ExecuteAction('volumeup') == 'OK'
+ self.update_ha_state()
+
+ def volume_down(self):
+ """ volume_down media player. """
+ assert self._server.Input.ExecuteAction('volumedown') == 'OK'
+ self.update_ha_state()
+
+ def set_volume_level(self, volume):
+ """ set volume level, range 0..1. """
+ self._server.Application.SetVolume(int(volume * 100))
+ self.update_ha_state()
+
+ def mute_volume(self, mute):
+ """ mute (true) or unmute (false) media player. """
+ self._server.Application.SetMute(mute)
+ self.update_ha_state()
+
+ def _set_play_state(self, state):
+ """ Helper method for play/pause/toggle """
+ players = self._get_players()
+
+ if len(players) != 0:
+ self._server.Player.PlayPause(players[0]['playerid'], state)
+
+ self.update_ha_state()
+
+ def media_play_pause(self):
+ """ media_play_pause media player. """
+ self._set_play_state('toggle')
+
+ def media_play(self):
+ """ media_play media player. """
+ self._set_play_state(True)
+
+ def media_pause(self):
+ """ media_pause media player. """
+ self._set_play_state(False)
+
+ def _goto(self, direction):
+ """ Helper method used for previous/next track """
+ players = self._get_players()
+
+ if len(players) != 0:
+ self._server.Player.GoTo(players[0]['playerid'], direction)
+
+ self.update_ha_state()
+
+ def media_next_track(self):
+ """ Send next track command. """
+ self._goto('next')
+
+ def media_previous_track(self):
+ """ Send next track command. """
+ # first seek to position 0, Kodi seems to go to the beginning
+ # of the current track current track is not at the beginning
+ self.media_seek(0)
+ self._goto('previous')
+
+ def media_seek(self, position):
+ """ Send seek command. """
+ players = self._get_players()
+
+ time = {}
+
+ time['milliseconds'] = int((position % 1) * 1000)
+ position = int(position)
+
+ time['seconds'] = int(position % 60)
+ position /= 60
+
+ time['minutes'] = int(position % 60)
+ position /= 60
+
+ time['hours'] = int(position)
+
+ if len(players) != 0:
+ self._server.Player.Seek(players[0]['playerid'], time)
+
+ self.update_ha_state()
+
+ def turn_on(self):
+ """ turn the media player on. """
+ raise NotImplementedError()
+
+ def play_youtube(self, media_id):
+ """ Plays a YouTube media. """
+ raise NotImplementedError()
diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py
index 53faa37a605..f8b455ae6fe 100644
--- a/homeassistant/components/media_player/mpd.py
+++ b/homeassistant/components/media_player/mpd.py
@@ -32,16 +32,28 @@ Location of your Music Player Daemon.
import logging
import socket
+try:
+ import mpd
+except ImportError:
+ mpd = None
+
+
+from homeassistant.const import (
+ STATE_PLAYING, STATE_PAUSED, STATE_OFF)
+
from homeassistant.components.media_player import (
- MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
- ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST,
- ATTR_MEDIA_ALBUM, ATTR_MEDIA_DATE, ATTR_MEDIA_DURATION,
- ATTR_MEDIA_VOLUME, MEDIA_STATE_PAUSED, MEDIA_STATE_PLAYING,
- MEDIA_STATE_STOPPED, MEDIA_STATE_UNKNOWN)
+ MediaPlayerDevice,
+ SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
+ SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
+ MEDIA_TYPE_MUSIC)
_LOGGER = logging.getLogger(__name__)
+SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
+
+
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the MPD platform. """
@@ -50,10 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get('port', 6600)
location = config.get('location', 'MPD')
- try:
- from mpd import MPDClient
-
- except ImportError:
+ if mpd is None:
_LOGGER.exception(
"Unable to import mpd2. "
"Did you maybe not install the 'python-mpd2' package?")
@@ -62,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=no-member
try:
- mpd_client = MPDClient()
+ mpd_client = mpd.MPDClient()
mpd_client.connect(daemon, port)
mpd_client.close()
mpd_client.disconnect()
@@ -73,110 +82,112 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
- mpd = []
- mpd.append(MpdDevice(daemon, port, location))
- add_devices(mpd)
+ add_devices([MpdDevice(daemon, port, location)])
class MpdDevice(MediaPlayerDevice):
""" Represents a MPD server. """
- def __init__(self, server, port, location):
- from mpd import MPDClient
+ # MPD confuses pylint
+ # pylint: disable=no-member, abstract-method
+ def __init__(self, server, port, location):
self.server = server
self.port = port
self._name = location
- self.state_attr = {ATTR_MEDIA_STATE: MEDIA_STATE_STOPPED}
+ self.status = None
+ self.currentsong = None
- self.client = MPDClient()
+ self.client = mpd.MPDClient()
self.client.timeout = 10
self.client.idletimeout = None
- self.client.connect(self.server, self.port)
+ self.update()
+
+ def update(self):
+ try:
+ self.status = self.client.status()
+ self.currentsong = self.client.currentsong()
+ except mpd.ConnectionError:
+ self.client.connect(self.server, self.port)
+ self.status = self.client.status()
+ self.currentsong = self.client.currentsong()
@property
def name(self):
""" Returns the name of the device. """
return self._name
- # pylint: disable=no-member
@property
def state(self):
- """ Returns the state of the device. """
- status = self.client.status()
-
- if status is None:
- return STATE_NO_APP
- else:
- return self.client.currentsong()['artist']
-
- @property
- def media_state(self):
""" Returns the media state. """
- media_controller = self.client.status()
-
- if media_controller['state'] == 'play':
- return MEDIA_STATE_PLAYING
- elif media_controller['state'] == 'pause':
- return MEDIA_STATE_PAUSED
- elif media_controller['state'] == 'stop':
- return MEDIA_STATE_STOPPED
+ if self.status['state'] == 'play':
+ return STATE_PLAYING
+ elif self.status['state'] == 'pause':
+ return STATE_PAUSED
else:
- return MEDIA_STATE_UNKNOWN
+ return STATE_OFF
- # pylint: disable=no-member
@property
- def state_attributes(self):
- """ Returns the state attributes. """
- status = self.client.status()
- current_song = self.client.currentsong()
+ def media_content_id(self):
+ """ Content ID of current playing media. """
+ return self.currentsong['id']
- if not status and not current_song:
- state_attr = {}
+ @property
+ def media_content_type(self):
+ """ Content type of current playing media. """
+ return MEDIA_TYPE_MUSIC
- if current_song['id']:
- state_attr[ATTR_MEDIA_CONTENT_ID] = current_song['id']
+ @property
+ def media_duration(self):
+ """ Duration of current playing media in seconds. """
+ # Time does not exist for streams
+ return self.currentsong.get('time')
- if current_song['date']:
- state_attr[ATTR_MEDIA_DATE] = current_song['date']
+ @property
+ def media_title(self):
+ """ Title of current playing media. """
+ return self.currentsong['title']
- if current_song['title']:
- state_attr[ATTR_MEDIA_TITLE] = current_song['title']
+ @property
+ def media_artist(self):
+ """ Artist of current playing media. (Music track only) """
+ return self.currentsong.get('artist')
- if current_song['time']:
- state_attr[ATTR_MEDIA_DURATION] = current_song['time']
+ @property
+ def media_album_name(self):
+ """ Album of current playing media. (Music track only) """
+ return self.currentsong.get('album')
- if current_song['artist']:
- state_attr[ATTR_MEDIA_ARTIST] = current_song['artist']
+ @property
+ def volume_level(self):
+ return int(self.status['volume'])/100
- if current_song['album']:
- state_attr[ATTR_MEDIA_ALBUM] = current_song['album']
-
- state_attr[ATTR_MEDIA_VOLUME] = status['volume']
-
- return state_attr
+ @property
+ def supported_media_commands(self):
+ """ Flags of media commands that are supported. """
+ return SUPPORT_MPD
def turn_off(self):
""" Service to exit the running MPD. """
self.client.stop()
+ def set_volume_level(self, volume):
+ """ Sets volume """
+ self.client.setvol(int(volume * 100))
+
def volume_up(self):
""" Service to send the MPD the command for volume up. """
- current_volume = self.client.status()['volume']
+ current_volume = int(self.status['volume'])
- if int(current_volume) <= 100:
- self.client.setvol(int(current_volume) + 5)
+ if current_volume <= 100:
+ self.client.setvol(current_volume + 5)
def volume_down(self):
""" Service to send the MPD the command for volume down. """
- current_volume = self.client.status()['volume']
+ current_volume = int(self.status['volume'])
- if int(current_volume) >= 0:
- self.client.setvol(int(current_volume) - 5)
-
- def media_play_pause(self):
- """ Service to send the MPD the command for play/pause. """
- self.client.pause()
+ if current_volume >= 0:
+ self.client.setvol(current_volume - 5)
def media_play(self):
""" Service to send the MPD the command for play/pause. """
@@ -190,6 +201,6 @@ class MpdDevice(MediaPlayerDevice):
""" Service to send the MPD the command for next track. """
self.client.next()
- def media_prev_track(self):
+ def media_previous_track(self):
""" Service to send the MPD the command for previous track. """
self.client.previous()
diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py
index 90a0929e6c0..4f46915d870 100644
--- a/homeassistant/components/modbus.py
+++ b/homeassistant/components/modbus.py
@@ -97,5 +97,5 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
- # Tells the bootstrapper that the component was succesfully initialized
+ # Tells the bootstrapper that the component was successfully initialized
return True
diff --git a/homeassistant/components/notify/file.py b/homeassistant/components/notify/file.py
new file mode 100644
index 00000000000..186bc53ca98
--- /dev/null
+++ b/homeassistant/components/notify/file.py
@@ -0,0 +1,78 @@
+"""
+homeassistant.components.notify.file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+File notification service.
+
+Configuration:
+
+To use the File notifier you will need to add something like the following
+to your config/configuration.yaml
+
+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.
+"""
+import logging
+import os
+
+import homeassistant.util.dt as dt_util
+from homeassistant.helpers import validate_config
+from homeassistant.components.notify import (
+ DOMAIN, ATTR_TITLE, BaseNotificationService)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def get_service(hass, config):
+ """ Get the file notification service. """
+
+ if not validate_config(config,
+ {DOMAIN: ['filename',
+ 'timestamp']},
+ _LOGGER):
+ return None
+
+ filename = config[DOMAIN]['filename']
+ timestamp = config[DOMAIN]['timestamp']
+
+ return FileNotificationService(hass, filename, timestamp)
+
+
+# pylint: disable=too-few-public-methods
+class FileNotificationService(BaseNotificationService):
+ """ Implements notification service for the File service. """
+
+ def __init__(self, hass, filename, add_timestamp):
+ self.filepath = os.path.join(hass.config.config_dir, filename)
+ self.add_timestamp = add_timestamp
+
+ def send_message(self, message="", **kwargs):
+ """ Send a message to a file. """
+
+ with open(self.filepath, 'a') as file:
+ if os.stat(self.filepath).st_size == 0:
+ title = '{} notifications (Log started: {})\n{}\n'.format(
+ kwargs.get(ATTR_TITLE),
+ dt_util.strip_microseconds(dt_util.utcnow()),
+ '-'*80)
+ file.write(title)
+
+ if self.add_timestamp == 1:
+ text = '{} {}\n'.format(dt_util.utcnow(), message)
+ file.write(text)
+ else:
+ text = '{}\n'.format(message)
+ file.write(text)
diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py
new file mode 100644
index 00000000000..6673ba23a08
--- /dev/null
+++ b/homeassistant/components/notify/syslog.py
@@ -0,0 +1,110 @@
+"""
+homeassistant.components.notify.syslog
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Syslog notification service.
+
+Configuration:
+
+To use the Syslog notifier you will need to add something like the following
+to your config/configuration.yaml
+
+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.
+"""
+import logging
+import syslog
+
+from homeassistant.helpers import validate_config
+from homeassistant.components.notify import (
+ DOMAIN, ATTR_TITLE, BaseNotificationService)
+
+_LOGGER = logging.getLogger(__name__)
+FACILITIES = {'kernel': syslog.LOG_KERN,
+ 'user': syslog.LOG_USER,
+ 'mail': syslog.LOG_MAIL,
+ 'daemon': syslog.LOG_DAEMON,
+ 'auth': syslog.LOG_KERN,
+ 'LPR': syslog.LOG_LPR,
+ 'news': syslog.LOG_NEWS,
+ 'uucp': syslog.LOG_UUCP,
+ 'cron': syslog.LOG_CRON,
+ 'syslog': syslog.LOG_SYSLOG,
+ 'local0': syslog.LOG_LOCAL0,
+ 'local1': syslog.LOG_LOCAL1,
+ 'local2': syslog.LOG_LOCAL2,
+ 'local3': syslog.LOG_LOCAL3,
+ 'local4': syslog.LOG_LOCAL4,
+ 'local5': syslog.LOG_LOCAL5,
+ 'local6': syslog.LOG_LOCAL6,
+ 'local7': syslog.LOG_LOCAL7}
+
+OPTIONS = {'pid': syslog.LOG_PID,
+ 'cons': syslog.LOG_CONS,
+ 'ndelay': syslog.LOG_NDELAY,
+ 'nowait': syslog.LOG_NOWAIT,
+ 'perror': syslog.LOG_PERROR}
+
+PRIORITIES = {5: syslog.LOG_EMERG,
+ 4: syslog.LOG_ALERT,
+ 3: syslog.LOG_CRIT,
+ 2: syslog.LOG_ERR,
+ 1: syslog.LOG_WARNING,
+ 0: syslog.LOG_NOTICE,
+ -1: syslog.LOG_INFO,
+ -2: syslog.LOG_DEBUG}
+
+
+def get_service(hass, config):
+ """ Get the mail notification service. """
+
+ if not validate_config(config,
+ {DOMAIN: ['facility',
+ 'option',
+ 'priority']},
+ _LOGGER):
+ return None
+
+ _facility = FACILITIES.get(config[DOMAIN]['facility'], 40)
+ _option = OPTIONS.get(config[DOMAIN]['option'], 10)
+ _priority = PRIORITIES.get(config[DOMAIN]['priority'], -1)
+
+ return SyslogNotificationService(_facility, _option, _priority)
+
+
+# pylint: disable=too-few-public-methods
+class SyslogNotificationService(BaseNotificationService):
+ """ Implements syslog notification service. """
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, facility, option, priority):
+ self._facility = facility
+ self._option = option
+ self._priority = priority
+
+ def send_message(self, message="", **kwargs):
+ """ Send a message to a user. """
+
+ title = kwargs.get(ATTR_TITLE)
+
+ syslog.openlog(title, self._option, self._facility)
+ syslog.syslog(self._priority, message)
+ syslog.closelog()
diff --git a/homeassistant/components/sensor/arduino.py b/homeassistant/components/sensor/arduino.py
new file mode 100644
index 00000000000..4210a064d13
--- /dev/null
+++ b/homeassistant/components/sensor/arduino.py
@@ -0,0 +1,87 @@
+"""
+homeassistant.components.sensor.arduino
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Support for getting information from Arduino pins. Only analog pins are
+supported.
+
+Configuration:
+
+sensor:
+ platform: arduino
+ pins:
+ 7:
+ name: Door switch
+ type: analog
+ 0:
+ name: Brightness
+ type: analog
+
+Variables:
+
+pins
+*Required
+An array specifying the digital pins to use on the Arduino board.
+
+These are the variables for the pins array:
+
+name
+*Required
+The name for the pin that will be used in the frontend.
+
+type
+*Required
+The type of the pin: 'analog'.
+"""
+import logging
+
+import homeassistant.components.arduino as arduino
+from homeassistant.helpers.entity import Entity
+from homeassistant.const import DEVICE_DEFAULT_NAME
+
+DEPENDENCIES = ['arduino']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """ Sets up the Arduino platform. """
+
+ # Verify that Arduino board is present
+ if arduino.BOARD is None:
+ _LOGGER.error('A connection has not been made to the Arduino board.')
+ return False
+
+ sensors = []
+ pins = config.get('pins')
+ for pinnum, pin in pins.items():
+ if pin.get('name'):
+ sensors.append(ArduinoSensor(pin.get('name'),
+ pinnum,
+ 'analog'))
+ add_devices(sensors)
+
+
+class ArduinoSensor(Entity):
+ """ Represents an Arduino Sensor. """
+ def __init__(self, name, pin, pin_type):
+ self._pin = pin
+ self._name = name or DEVICE_DEFAULT_NAME
+ self.pin_type = pin_type
+ self.direction = 'in'
+ self._value = None
+
+ arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type)
+
+ @property
+ def state(self):
+ """ Returns the state of the sensor. """
+ return self._value
+
+ @property
+ def name(self):
+ """ Get the name of the sensor. """
+ return self._name
+
+ def update(self):
+ """ Get the latest value from the pin. """
+ self._value = arduino.BOARD.get_analog_inputs()[self._pin][1]
diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py
index 8d174981900..bc29198e6a0 100644
--- a/homeassistant/components/sensor/bitcoin.py
+++ b/homeassistant/components/sensor/bitcoin.py
@@ -113,7 +113,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Unable to import blockchain. "
"Did you maybe not install the 'blockchain' package?")
- return None
+ return False
wallet_id = config.get('wallet', None)
password = config.get('password', None)
diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py
new file mode 100644
index 00000000000..98e088d5139
--- /dev/null
+++ b/homeassistant/components/sensor/forecast.py
@@ -0,0 +1,216 @@
+"""
+homeassistant.components.sensor.forecast
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Forecast.io service.
+
+Configuration:
+
+To use the Forecast sensor you will need to add something like the
+following to your config/configuration.yaml
+
+sensor:
+ platform: forecast
+ api_key: YOUR_APP_KEY
+ monitored_conditions:
+ - summary
+ - precip_type
+ - precip_intensity
+ - temperature
+ - dew_point
+ - wind_speed
+ - wind_bearing
+ - cloud_cover
+ - humidity
+ - pressure
+ - visibility
+ - ozone
+
+Variables:
+
+api_key
+*Required
+To retrieve this value log into your account at http://forecast.io/. You can
+make 1000 requests per day. This means that you could create every 1.4 minute
+one.
+
+monitored_conditions
+*Required
+An array specifying the conditions to monitor.
+
+These are the variables for the monitored_conditions array:
+
+type
+*Required
+The condition you wish to monitor, see the configuration example above for a
+list of all available conditions to monitor.
+
+Details for the API : https://developer.forecast.io/docs/v2
+"""
+import logging
+from datetime import timedelta
+
+import forecastio
+
+from homeassistant.util import Throttle
+from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+SENSOR_TYPES = {
+ 'summary': ['Summary', ''],
+ 'precip_type': ['Precip', ''],
+ 'precip_intensity': ['Precip intensity', 'mm'],
+ 'temperature': ['Temperature', ''],
+ 'dew_point': ['Dew point', '°C'],
+ 'wind_speed': ['Wind Speed', 'm/s'],
+ 'wind_bearing': ['Wind Bearing', '°'],
+ 'cloud_cover': ['Cloud coverage', '%'],
+ 'humidity': ['Humidity', '%'],
+ 'pressure': ['Pressure', 'mBar'],
+ 'visibility': ['Visibility', 'km'],
+ 'ozone': ['Ozone', ''],
+}
+
+# Return cached results if last scan was less then this time ago
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """ Get the Forecast.io sensor. """
+
+ if None in (hass.config.latitude, hass.config.longitude):
+ _LOGGER.error("Latitude or longitude not set in Home Assistant config")
+ return False
+
+ SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
+ unit = hass.config.temperature_unit
+
+ try:
+ forecast = forecastio.load_forecast(config.get(CONF_API_KEY, None),
+ hass.config.latitude,
+ hass.config.longitude)
+ forecast.currently()
+ except ValueError:
+ _LOGGER.error(
+ "Connection error "
+ "Please check your settings for Forecast.io.")
+ return False
+
+ data = ForeCastData(config.get(CONF_API_KEY, None),
+ hass.config.latitude,
+ hass.config.longitude)
+
+ dev = []
+ for variable in config['monitored_conditions']:
+ if variable not in SENSOR_TYPES:
+ _LOGGER.error('Sensor type: "%s" does not exist', variable)
+ else:
+ dev.append(ForeCastSensor(data, variable, unit))
+
+ add_devices(dev)
+
+
+# pylint: disable=too-few-public-methods
+class ForeCastSensor(Entity):
+ """ Implements an OpenWeatherMap sensor. """
+
+ def __init__(self, weather_data, sensor_type, unit):
+ self.client_name = 'Forecast'
+ self._name = SENSOR_TYPES[sensor_type][0]
+ self.forecast_client = weather_data
+ self._unit = unit
+ self.type = sensor_type
+ self._state = None
+ self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
+ self.update()
+
+ @property
+ def name(self):
+ return '{} - {}'.format(self.client_name, self._name)
+
+ @property
+ def state(self):
+ """ Returns the state of the device. """
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """ Unit of measurement of this entity, if any. """
+ return self._unit_of_measurement
+
+ # pylint: disable=too-many-branches
+ def update(self):
+ """ Gets the latest data from Forecast.io and updates the states. """
+
+ self.forecast_client.update()
+ data = self.forecast_client.data
+
+ try:
+ if self.type == 'summary':
+ self._state = data.summary
+ # elif self.type == 'sunrise_time':
+ # self._state = data.sunriseTime
+ # elif self.type == 'sunset_time':
+ # self._state = data.sunsetTime
+ elif self.type == 'precip_intensity':
+ if data.precipIntensity == 0:
+ self._state = 'None'
+ self._unit_of_measurement = ''
+ else:
+ self._state = data.precipIntensity
+ elif self.type == 'precip_type':
+ if data.precipType is None:
+ self._state = 'None'
+ self._unit_of_measurement = ''
+ else:
+ self._state = data.precipType
+ elif self.type == 'dew_point':
+ if self._unit == TEMP_CELCIUS:
+ self._state = round(data.dewPoint, 1)
+ elif self._unit == TEMP_FAHRENHEIT:
+ self._state = round(data.dewPoint * 1.8 + 32.0, 1)
+ else:
+ self._state = round(data.dewPoint, 1)
+ elif self.type == 'temperature':
+ if self._unit == TEMP_CELCIUS:
+ self._state = round(data.temperature, 1)
+ elif self._unit == TEMP_FAHRENHEIT:
+ self._state = round(data.temperature * 1.8 + 32.0, 1)
+ else:
+ self._state = round(data.temperature, 1)
+ elif self.type == 'wind_speed':
+ self._state = data.windSpeed
+ elif self.type == 'wind_bearing':
+ self._state = data.windBearing
+ elif self.type == 'cloud_cover':
+ self._state = round(data.cloudCover * 100, 1)
+ elif self.type == 'humidity':
+ self._state = round(data.humidity * 100, 1)
+ elif self.type == 'pressure':
+ self._state = round(data.pressure, 1)
+ elif self.type == 'visibility':
+ self._state = data.visibility
+ elif self.type == 'ozone':
+ self._state = round(data.ozone, 1)
+ except forecastio.utils.PropertyUnavailable:
+ pass
+
+
+class ForeCastData(object):
+ """ Gets the latest data from Forecast.io. """
+
+ def __init__(self, api_key, latitude, longitude):
+ self._api_key = api_key
+ self.latitude = latitude
+ self.longitude = longitude
+ self.data = None
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """ Gets the latest data from Forecast.io. """
+
+ forecast = forecastio.load_forecast(self._api_key,
+ self.latitude,
+ self.longitude)
+ self.data = forecast.currently()
diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py
index dbbd23322c0..07413e7b1ea 100644
--- a/homeassistant/components/sensor/openweathermap.py
+++ b/homeassistant/components/sensor/openweathermap.py
@@ -1,7 +1,6 @@
"""
homeassistant.components.sensor.openweathermap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
OpenWeatherMap (OWM) service.
Configuration:
@@ -12,7 +11,8 @@ following to your config/configuration.yaml
sensor:
platform: openweathermap
api_key: YOUR_APP_KEY
- monitored_variables:
+ forecast: 0 or 1
+ monitored_conditions:
- weather
- temperature
- wind_speed
@@ -28,15 +28,13 @@ api_key
*Required
To retrieve this value log into your account at http://openweathermap.org/
+forecast
+*Optional
+Enables the forecast. The default is to display the current conditions.
+
monitored_conditions
-*Required
-An array specifying the variables to monitor.
-
-These are the variables for the monitored_conditions array:
-
-type
-*Required
-The variable you wish to monitor, see the configuration example above for a
+*Optional
+Conditions to monitor. See the configuration example above for a
list of all available conditions to monitor.
Details for the API : http://bugs.openweathermap.org/projects/api/wiki
@@ -81,10 +79,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Unable to import pyowm. "
"Did you maybe not install the 'PyOWM' package?")
- return None
+ return False
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
+ forecast = config.get('forecast', 0)
owm = OWM(config.get(CONF_API_KEY, None))
if not owm:
@@ -93,13 +92,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Please check your settings for OpenWeatherMap.")
return None
- data = WeatherData(owm, hass.config.latitude, hass.config.longitude)
+ data = WeatherData(owm, forecast, hass.config.latitude,
+ hass.config.longitude)
dev = []
- for variable in config['monitored_conditions']:
- if variable not in SENSOR_TYPES:
- _LOGGER.error('Sensor type: "%s" does not exist', variable)
- else:
- dev.append(OpenWeatherMapSensor(data, variable, unit))
+ try:
+ for variable in config['monitored_conditions']:
+ if variable not in SENSOR_TYPES:
+ _LOGGER.error('Sensor type: "%s" does not exist', variable)
+ else:
+ dev.append(OpenWeatherMapSensor(data, variable, unit))
+ except KeyError:
+ pass
+
+ if forecast == 1:
+ SENSOR_TYPES['forecast'] = ['Forecast', '']
+ dev.append(OpenWeatherMapSensor(data, 'forecast', unit))
add_devices(dev)
@@ -108,11 +115,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class OpenWeatherMapSensor(Entity):
""" Implements an OpenWeatherMap sensor. """
- def __init__(self, weather_data, sensor_type, unit):
- self.client_name = 'Weather - '
+ def __init__(self, weather_data, sensor_type, temp_unit):
+ self.client_name = 'Weather'
self._name = SENSOR_TYPES[sensor_type][0]
self.owa_client = weather_data
- self._unit = unit
+ self.temp_unit = temp_unit
self.type = sensor_type
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@@ -120,7 +127,7 @@ class OpenWeatherMapSensor(Entity):
@property
def name(self):
- return self.client_name + ' ' + self._name
+ return '{} {}'.format(self.client_name, self._name)
@property
def state(self):
@@ -138,14 +145,15 @@ class OpenWeatherMapSensor(Entity):
self.owa_client.update()
data = self.owa_client.data
+ fc_data = self.owa_client.fc_data
if self.type == 'weather':
self._state = data.get_detailed_status()
elif self.type == 'temperature':
- if self._unit == TEMP_CELCIUS:
+ if self.temp_unit == TEMP_CELCIUS:
self._state = round(data.get_temperature('celsius')['temp'],
1)
- elif self._unit == TEMP_FAHRENHEIT:
+ elif self.temp_unit == TEMP_FAHRENHEIT:
self._state = round(data.get_temperature('fahrenheit')['temp'],
1)
else:
@@ -161,29 +169,39 @@ class OpenWeatherMapSensor(Entity):
elif self.type == 'rain':
if data.get_rain():
self._state = round(data.get_rain()['3h'], 0)
+ self._unit_of_measurement = 'mm'
else:
self._state = 'not raining'
self._unit_of_measurement = ''
elif self.type == 'snow':
if data.get_snow():
self._state = round(data.get_snow(), 0)
+ self._unit_of_measurement = 'mm'
else:
self._state = 'not snowing'
self._unit_of_measurement = ''
+ elif self.type == 'forecast':
+ self._state = fc_data.get_weathers()[0].get_status()
class WeatherData(object):
""" Gets the latest data from OpenWeatherMap. """
- def __init__(self, owm, latitude, longitude):
+ def __init__(self, owm, forecast, latitude, longitude):
self.owm = owm
+ self.forecast = forecast
self.latitude = latitude
self.longitude = longitude
self.data = None
+ self.fc_data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from OpenWeatherMap. """
-
obs = self.owm.weather_at_coords(self.latitude, self.longitude)
self.data = obs.get_weather()
+
+ if self.forecast == 1:
+ obs = self.owm.three_hours_forecast_at_coords(self.latitude,
+ self.longitude)
+ self.fc_data = obs.get_forecast()
diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py
index a9eda2754bd..bd2d336d8ed 100644
--- a/homeassistant/components/sensor/swiss_public_transport.py
+++ b/homeassistant/components/sensor/swiss_public_transport.py
@@ -122,7 +122,7 @@ class PublicTransportData(object):
try:
return [
- dt_util.datetime_to_short_time_str(
+ dt_util.datetime_to_time_str(
dt_util.as_local(dt_util.utc_from_timestamp(
item['from']['departureTimestamp']))
)
diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py
index 0f9d570f92a..2615ab1a77b 100644
--- a/homeassistant/components/sensor/systemmonitor.py
+++ b/homeassistant/components/sensor/systemmonitor.py
@@ -21,9 +21,26 @@ sensor:
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
+ - type: 'swap_use_percent'
+ - type: 'swap_use'
+ - type: 'swap_free'
+ - type: 'network_in'
+ arg: 'eth0'
+ - type: 'network_out'
+ arg: 'eth0'
+ - type: 'packets_in'
+ arg: 'eth0'
+ - type: 'packets_out'
+ arg: 'eth0'
+ - type: 'ipv4_address'
+ arg: 'eth0'
+ - type: 'ipv6_address'
+ arg: 'eth0'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
+ - type: 'last_boot'
+ - type: 'since_last_boot'
Variables:
@@ -42,12 +59,12 @@ arg
*Optional
Additional details for the type, eg. path, binary name, etc.
"""
+import logging
+import psutil
+import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.const import STATE_ON, STATE_OFF
-import psutil
-import logging
-
SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'],
@@ -58,6 +75,17 @@ SENSOR_TYPES = {
'memory_free': ['RAM Free', 'MiB'],
'processor_use': ['CPU Use', '%'],
'process': ['Process', ''],
+ 'swap_use_percent': ['Swap Use', '%'],
+ 'swap_use': ['Swap Use', 'GiB'],
+ 'swap_free': ['Swap Free', 'GiB'],
+ 'network_out': ['Sent', 'MiB'],
+ 'network_in': ['Recieved', 'MiB'],
+ 'packets_out': ['Packets sent', ''],
+ 'packets_in': ['Packets recieved', ''],
+ 'ipv4_address': ['IPv4 address', ''],
+ 'ipv6_address': ['IPv6 address', ''],
+ 'last_boot': ['Last Boot', ''],
+ 'since_last_boot': ['Since Last Boot', '']
}
_LOGGER = logging.getLogger(__name__)
@@ -103,6 +131,7 @@ class SystemMonitorSensor(Entity):
def unit_of_measurement(self):
return self._unit_of_measurement
+ # pylint: disable=too-many-branches
def update(self):
if self.type == 'disk_use_percent':
self._state = psutil.disk_usage(self.argument).percent
@@ -120,6 +149,12 @@ class SystemMonitorSensor(Entity):
1024**2, 1)
elif self.type == 'memory_free':
self._state = round(psutil.virtual_memory().available / 1024**2, 1)
+ elif self.type == 'swap_use_percent':
+ self._state = psutil.swap_memory().percent
+ elif self.type == 'swap_use':
+ self._state = round(psutil.swap_memory().used / 1024**3, 1)
+ elif self.type == 'swap_free':
+ self._state = round(psutil.swap_memory().free / 1024**3, 1)
elif self.type == 'processor_use':
self._state = round(psutil.cpu_percent(interval=None))
elif self.type == 'process':
@@ -127,3 +162,24 @@ class SystemMonitorSensor(Entity):
self._state = STATE_ON
else:
self._state = STATE_OFF
+ elif self.type == 'network_out':
+ self._state = round(psutil.net_io_counters(pernic=True)
+ [self.argument][0] / 1024**2, 1)
+ elif self.type == 'network_in':
+ self._state = round(psutil.net_io_counters(pernic=True)
+ [self.argument][1] / 1024**2, 1)
+ elif self.type == 'packets_out':
+ self._state = psutil.net_io_counters(pernic=True)[self.argument][2]
+ elif self.type == 'packets_in':
+ self._state = psutil.net_io_counters(pernic=True)[self.argument][3]
+ elif self.type == 'ipv4_address':
+ self._state = psutil.net_if_addrs()[self.argument][0][1]
+ elif self.type == 'ipv6_address':
+ self._state = psutil.net_if_addrs()[self.argument][1][1]
+ elif self.type == 'last_boot':
+ self._state = dt_util.datetime_to_date_str(
+ dt_util.as_local(
+ dt_util.utc_from_timestamp(psutil.boot_time())))
+ elif self.type == 'since_last_boot':
+ self._state = dt_util.utcnow() - dt_util.utc_from_timestamp(
+ psutil.boot_time())
diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py
index cd35e8343ba..149730d4b5b 100644
--- a/homeassistant/components/sensor/time_date.py
+++ b/homeassistant/components/sensor/time_date.py
@@ -12,24 +12,18 @@ following to your config/configuration.yaml
sensor:
platform: time_date
display_options:
- - type: 'time'
- - type: 'date'
- - type: 'date_time'
- - type: 'time_date'
- - type: 'time_utc'
- - type: 'beat'
+ - 'time'
+ - 'date'
+ - 'date_time'
+ - 'time_date'
+ - 'time_utc'
+ - 'beat'
Variables:
display_options
*Required
-An array specifying the variables to display.
-
-These are the variables for the display_options array.:
-
-type
-*Required
-The variable you wish to display, see the configuration example above for a
+The variable you wish to display. See the configuration example above for a
list of all available variables.
"""
import logging
@@ -57,10 +51,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for variable in config['display_options']:
- if variable['type'] not in OPTION_TYPES:
- _LOGGER.error('Option type: "%s" does not exist', variable['type'])
+ if variable not in OPTION_TYPES:
+ _LOGGER.error('Option type: "%s" does not exist', variable)
else:
- dev.append(TimeDateSensor(variable['type']))
+ dev.append(TimeDateSensor(variable))
add_devices(dev)
@@ -89,9 +83,9 @@ class TimeDateSensor(Entity):
""" Gets the latest data and updates the states. """
time_date = dt_util.utcnow()
- time = dt_util.datetime_to_short_time_str(dt_util.as_local(time_date))
- time_utc = dt_util.datetime_to_short_time_str(time_date)
- date = dt_util.datetime_to_short_date_str(dt_util.as_local(time_date))
+ time = dt_util.datetime_to_time_str(dt_util.as_local(time_date))
+ time_utc = dt_util.datetime_to_time_str(time_date)
+ date = dt_util.datetime_to_date_str(dt_util.as_local(time_date))
# Calculate the beat (Swatch Internet Time) time without date.
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')
diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py
index 359839a3946..95457b66f5f 100644
--- a/homeassistant/components/switch/__init__.py
+++ b/homeassistant/components/switch/__init__.py
@@ -7,6 +7,7 @@ import logging
from datetime import timedelta
from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@@ -33,6 +34,11 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SWITCHES: 'isy994',
}
+PROP_TO_ATTR = {
+ 'current_power_mwh': ATTR_CURRENT_POWER_MWH,
+ 'today_power_mw': ATTR_TODAY_MWH,
+}
+
_LOGGER = logging.getLogger(__name__)
@@ -74,10 +80,48 @@ def setup(hass, config):
else:
switch.turn_off()
- switch.update_ha_state(True)
+ if switch.should_poll:
+ switch.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
return True
+
+
+class SwitchDevice(ToggleEntity):
+ """ Represents a switch within Home Assistant. """
+ # pylint: disable=no-self-use
+
+ @property
+ def current_power_mwh(self):
+ """ Current power usage in mwh. """
+ return None
+
+ @property
+ def today_power_mw(self):
+ """ Today total power usage in mw. """
+ return None
+
+ @property
+ def device_state_attributes(self):
+ """ Returns device specific state attributes. """
+ return None
+
+ @property
+ def state_attributes(self):
+ """ Returns optional state attributes. """
+ data = {}
+
+ for prop, attr in PROP_TO_ATTR.items():
+ value = getattr(self, prop)
+ if value:
+ data[attr] = value
+
+ device_attr = self.device_state_attributes
+
+ if device_attr is not None:
+ data.update(device_attr)
+
+ return data
diff --git a/homeassistant/components/switch/arduino.py b/homeassistant/components/switch/arduino.py
new file mode 100644
index 00000000000..367e7378b27
--- /dev/null
+++ b/homeassistant/components/switch/arduino.py
@@ -0,0 +1,93 @@
+"""
+homeassistant.components.switch.arduino
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Support for switching Arduino pins on and off. So fare only digital pins are
+supported.
+
+Configuration:
+
+switch:
+ platform: arduino
+ pins:
+ 11:
+ name: Fan Office
+ type: digital
+ 12:
+ name: Light Desk
+ type: digital
+
+Variables:
+
+pins
+*Required
+An array specifying the digital pins to use on the Arduino board.
+
+These are the variables for the pins array:
+
+name
+*Required
+The name for the pin that will be used in the frontend.
+
+type
+*Required
+The type of the pin: 'digital'.
+"""
+import logging
+
+import homeassistant.components.arduino as arduino
+from homeassistant.components.switch import SwitchDevice
+from homeassistant.const import DEVICE_DEFAULT_NAME
+
+DEPENDENCIES = ['arduino']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """ Sets up the Arduino platform. """
+
+ # Verify that Arduino board is present
+ if arduino.BOARD is None:
+ _LOGGER.error('A connection has not been made to the Arduino board.')
+ return False
+
+ switches = []
+ pins = config.get('pins')
+ for pinnum, pin in pins.items():
+ if pin.get('name'):
+ switches.append(ArduinoSwitch(pin.get('name'),
+ pinnum,
+ pin.get('type')))
+ add_devices(switches)
+
+
+class ArduinoSwitch(SwitchDevice):
+ """ Represents an Arduino Switch. """
+ def __init__(self, name, pin, pin_type):
+ self._pin = pin
+ self._name = name or DEVICE_DEFAULT_NAME
+ self.pin_type = pin_type
+ self.direction = 'out'
+ self._state = False
+
+ arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type)
+
+ @property
+ def name(self):
+ """ Get the name of the pin. """
+ return self._name
+
+ @property
+ def is_on(self):
+ """ Returns True if pin is high/on. """
+ return self._state
+
+ def turn_on(self):
+ """ Turns the pin to high/on. """
+ self._state = True
+ arduino.BOARD.set_digital_out_high(self._pin)
+
+ def turn_off(self):
+ """ Turns the pin to low/off. """
+ self._state = False
+ arduino.BOARD.set_digital_out_low(self._pin)
diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py
index 291cd3af04d..7cc51a1f9b9 100644
--- a/homeassistant/components/switch/command_switch.py
+++ b/homeassistant/components/switch/command_switch.py
@@ -6,8 +6,7 @@ homeassistant.components.switch.command_switch
Allows to configure custom shell commands to turn a switch on/off.
"""
import logging
-from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
+from homeassistant.components.switch import SwitchDevice
import subprocess
_LOGGER = logging.getLogger(__name__)
@@ -30,11 +29,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(devices)
-class CommandSwitch(ToggleEntity):
+class CommandSwitch(SwitchDevice):
""" Represents a switch that can be togggled using shell commands """
def __init__(self, name, command_on, command_off):
- self._name = name or DEVICE_DEFAULT_NAME
- self._state = STATE_OFF
+ self._name = name
+ self._state = False
self._command_on = command_on
self._command_off = command_off
@@ -60,22 +59,19 @@ class CommandSwitch(ToggleEntity):
""" The name of the switch """
return self._name
- @property
- def state(self):
- """ Returns the state of the switch. """
- return self._state
-
@property
def is_on(self):
""" True if device is on. """
- return self._state == STATE_ON
+ return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
if CommandSwitch._switch(self._command_on):
- self._state = STATE_ON
+ self._state = True
+ self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
if CommandSwitch._switch(self._command_off):
- self._state = STATE_OFF
+ self._state = False
+ self.update_ha_state()
diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py
index b54b48a1c9b..7b2f077d6a8 100644
--- a/homeassistant/components/switch/demo.py
+++ b/homeassistant/components/switch/demo.py
@@ -5,20 +5,20 @@ homeassistant.components.switch.demo
Demo platform that has two fake switches.
"""
-from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
+from homeassistant.components.switch import SwitchDevice
+from homeassistant.const import DEVICE_DEFAULT_NAME
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo switches. """
add_devices_callback([
- DemoSwitch('Ceiling', STATE_ON),
- DemoSwitch('AC', STATE_OFF)
+ DemoSwitch('Ceiling', True),
+ DemoSwitch('AC', False)
])
-class DemoSwitch(ToggleEntity):
+class DemoSwitch(SwitchDevice):
""" Provides a demo switch. """
def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME
@@ -35,19 +35,27 @@ class DemoSwitch(ToggleEntity):
return self._name
@property
- def state(self):
- """ Returns the state of the device if any. """
- return self._state
+ def current_power_mwh(self):
+ """ Current power usage in mwh. """
+ if self._state:
+ return 100
+
+ @property
+ def today_power_mw(self):
+ """ Today total power usage in mw. """
+ return 1500
@property
def is_on(self):
""" True if device is on. """
- return self._state == STATE_ON
+ return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
- self._state = STATE_ON
+ self._state = True
+ self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
- self._state = STATE_OFF
+ self._state = False
+ self.update_ha_state()
diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py
index 84f5d2b31d5..6f9070d28fb 100644
--- a/homeassistant/components/switch/tellstick.py
+++ b/homeassistant/components/switch/tellstick.py
@@ -3,6 +3,12 @@ homeassistant.components.switch.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Tellstick switches.
+
+Because the tellstick sends its actions via radio and from most
+receivers it's impossible to know if the signal was received or not.
+Therefore you can configure the switch to try to send each signal repeatedly
+with the config parameter signal_repetitions (default is 1).
+signal_repetitions: 3
"""
import logging
@@ -11,6 +17,8 @@ from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
+SINGAL_REPETITIONS = 1
+
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@@ -22,6 +30,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return
+ signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
+
core = telldus.TelldusCore()
switches_and_lights = core.devices()
@@ -29,7 +39,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
for switch in switches_and_lights:
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
- switches.append(TellstickSwitchDevice(switch))
+ switches.append(TellstickSwitchDevice(switch, signal_repetitions))
add_devices_callback(switches)
@@ -39,9 +49,10 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)
- def __init__(self, tellstick):
+ def __init__(self, tellstick, signal_repetitions):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
+ self.signal_repetitions = signal_repetitions
@property
def name(self):
@@ -63,8 +74,10 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs):
""" Turns the switch on. """
- self.tellstick.turn_on()
+ for _ in range(self.signal_repetitions):
+ self.tellstick.turn_on()
def turn_off(self, **kwargs):
""" Turns the switch off. """
- self.tellstick.turn_off()
+ for _ in range(self.signal_repetitions):
+ self.tellstick.turn_off()
diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py
index d8be9286413..eb55e0662b7 100644
--- a/homeassistant/components/switch/wemo.py
+++ b/homeassistant/components/switch/wemo.py
@@ -6,9 +6,7 @@ Support for WeMo switches.
"""
import logging
-from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.components.switch import (
- ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
+from homeassistant.components.switch import SwitchDevice
# pylint: disable=unused-argument
@@ -43,10 +41,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if isinstance(switch, pywemo.Switch)])
-class WemoSwitch(ToggleEntity):
+class WemoSwitch(SwitchDevice):
""" Represents a WeMo switch within Home Assistant. """
def __init__(self, wemo):
self.wemo = wemo
+ self.insight_params = None
@property
def unique_id(self):
@@ -59,15 +58,16 @@ class WemoSwitch(ToggleEntity):
return self.wemo.name
@property
- def state_attributes(self):
- """ Returns optional state attributes. """
- if self.wemo.model.startswith('Belkin Insight'):
- cur_info = self.wemo.insight_params
+ def current_power_mwh(self):
+ """ Current power usage in mwh. """
+ if self.insight_params:
+ return self.insight_params['currentpower']
- return {
- ATTR_CURRENT_POWER_MWH: cur_info['currentpower'],
- ATTR_TODAY_MWH: cur_info['todaymw']
- }
+ @property
+ def today_power_mw(self):
+ """ Today total power usage in mw. """
+ if self.insight_params:
+ return self.insight_params['todaymw']
@property
def is_on(self):
@@ -85,3 +85,5 @@ class WemoSwitch(ToggleEntity):
def update(self):
""" Update WeMo state. """
self.wemo.get_state(True)
+ if self.wemo.model.startswith('Belkin Insight'):
+ self.insight_params = self.wemo.insight_params
diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py
index 5a99633041d..dd210bc2b7f 100644
--- a/homeassistant/components/wink.py
+++ b/homeassistant/components/wink.py
@@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
def update(self):
""" Update state of the light. """
- self.wink.wait_till_desired_reached()
+ self.wink.updateState()
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 1c48cc51774..7d58dbb01d2 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -40,6 +40,9 @@ STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
STATE_OPEN = 'open'
STATE_CLOSED = 'closed'
+STATE_PLAYING = 'playing'
+STATE_PAUSED = 'paused'
+STATE_IDLE = 'idle'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
@@ -104,7 +107,8 @@ SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
SERVICE_MEDIA_PLAY = "media_play"
SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
-SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
+SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
+SERVICE_MEDIA_SEEK = "media_seek"
# #### API / REMOTE ####
SERVER_PORT = 8123
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index fbe00c85527..d8fecf20db8 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -9,9 +9,9 @@ import datetime as dt
import pytz
-DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
-DATE_SHORT_STR_FORMAT = "%Y-%m-%d"
-TIME_SHORT_STR_FORMAT = "%H:%M"
+DATETIME_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
+DATE_STR_FORMAT = "%Y-%m-%d"
+TIME_STR_FORMAT = "%H:%M"
UTC = DEFAULT_TIME_ZONE = pytz.utc
@@ -34,7 +34,7 @@ def get_time_zone(time_zone_str):
def utcnow():
""" Get now in UTC time. """
- return dt.datetime.now(pytz.utc)
+ return dt.datetime.now(UTC)
def now(time_zone=None):
@@ -45,12 +45,12 @@ def now(time_zone=None):
def as_utc(dattim):
""" Return a datetime as UTC time.
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """
- if dattim.tzinfo == pytz.utc:
+ if dattim.tzinfo == UTC:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
- return dattim.astimezone(pytz.utc)
+ return dattim.astimezone(UTC)
def as_local(dattim):
@@ -58,17 +58,28 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim
elif dattim.tzinfo is None:
- dattim = dattim.replace(tzinfo=pytz.utc)
+ dattim = dattim.replace(tzinfo=UTC)
return dattim.astimezone(DEFAULT_TIME_ZONE)
def utc_from_timestamp(timestamp):
""" Returns a UTC time from a timestamp. """
- return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc)
+ return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC)
-def datetime_to_local_str(dattim, time_zone=None):
+def start_of_local_day(dt_or_d=None):
+ """ Return local datetime object of start of day from date or datetime. """
+ if dt_or_d is None:
+ dt_or_d = now().date()
+ elif isinstance(dt_or_d, dt.datetime):
+ dt_or_d = dt_or_d.date()
+
+ return dt.datetime.combine(dt_or_d, dt.time()).replace(
+ tzinfo=DEFAULT_TIME_ZONE)
+
+
+def datetime_to_local_str(dattim):
""" Converts datetime to specified time_zone and returns a string. """
return datetime_to_str(as_local(dattim))
@@ -76,27 +87,27 @@ def datetime_to_local_str(dattim, time_zone=None):
def datetime_to_str(dattim):
""" Converts datetime to a string format.
+ @rtype : str
+ """
+ return dattim.strftime(DATETIME_STR_FORMAT)
+
+
+def datetime_to_time_str(dattim):
+ """ Converts datetime to a string containing only the time.
+
+ @rtype : str
+ """
+ return dattim.strftime(TIME_STR_FORMAT)
+
+
+def datetime_to_date_str(dattim):
+ """ Converts datetime to a string containing only the date.
+
@rtype : str
"""
return dattim.strftime(DATE_STR_FORMAT)
-def datetime_to_short_time_str(dattim):
- """ Converts datetime to a string format as short time.
-
- @rtype : str
- """
- return dattim.strftime(TIME_SHORT_STR_FORMAT)
-
-
-def datetime_to_short_date_str(dattim):
- """ Converts datetime to a string format as short date.
-
- @rtype : str
- """
- return dattim.strftime(DATE_SHORT_STR_FORMAT)
-
-
def str_to_datetime(dt_str):
""" Converts a string to a UTC datetime object.
@@ -104,7 +115,15 @@ def str_to_datetime(dt_str):
"""
try:
return dt.datetime.strptime(
- dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc)
+ dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc)
+ except ValueError: # If dt_str did not match our format
+ return None
+
+
+def date_str_to_date(dt_str):
+ """ Converts a date string to a date object. """
+ try:
+ return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
except ValueError: # If dt_str did not match our format
return None
diff --git a/requirements.txt b/requirements.txt
index 04f4b122b8e..9f7e788f47c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,7 +18,7 @@ phue>=0.8
ledcontroller>=1.0.7
# Chromecast bindings (media_player.cast)
-pychromecast>=0.6.4
+pychromecast>=0.6.6
# Keyboard (keyboard)
pyuserinput>=0.1.9
@@ -39,10 +39,10 @@ python-nest>=2.3.1
pydispatcher>=2.0.5
# ISY994 bindings (*.isy994)
-PyISY>=1.0.2
+PyISY>=1.0.5
# PSutil (sensor.systemmonitor)
-psutil>=2.2.1
+psutil>=3.0.0
# Pushover bindings (notify.pushover)
python-pushover>=0.2
@@ -51,7 +51,7 @@ python-pushover>=0.2
transmissionrpc>=0.11
# OpenWeatherMap Web API (sensor.openweathermap)
-pyowm>=2.2.0
+pyowm>=2.2.1
# XMPP Bindings (notify.xmpp)
sleekxmpp>=1.3.1
@@ -64,3 +64,18 @@ python-mpd2>=0.5.4
# Hikvision (switch.hikvisioncam)
hikvision>=0.4
+
+# console log coloring
+colorlog>=2.6.0
+
+# JSON-RPC interface
+jsonrpc-requests>=0.1
+
+# Forecast.io Bindings (sensor.forecast)
+python-forecastio>=1.3.3
+
+# Firmata Bindings (*.arduino)
+PyMata==2.07a
+
+# Mysensors serial gateway
+pyserial>=2.7
diff --git a/tests/test_component_media_player.py b/tests/test_component_media_player.py
index fdde02e5594..b7f0b847e80 100644
--- a/tests/test_component_media_player.py
+++ b/tests/test_component_media_player.py
@@ -10,9 +10,10 @@ import unittest
import homeassistant as ha
from homeassistant.const import (
+ STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
- SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID)
+ SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID)
import homeassistant.components.media_player as media_player
from helpers import mock_service
@@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase):
self.hass = ha.HomeAssistant()
self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room')
- self.hass.states.set(self.test_entity, media_player.STATE_NO_APP)
+ self.hass.states.set(self.test_entity, STATE_OFF)
self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom')
self.hass.states.set(self.test_entity2, "YouTube")
@@ -56,7 +57,7 @@ class TestMediaPlayer(unittest.TestCase):
SERVICE_MEDIA_PLAY: media_player.media_play,
SERVICE_MEDIA_PAUSE: media_player.media_pause,
SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track,
- SERVICE_MEDIA_PREV_TRACK: media_player.media_prev_track
+ SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track
}
for service_name, service_method in services.items():