Merged in upstream changes

This commit is contained in:
jamespcole 2015-07-10 18:14:03 +10:00
commit 3440c54ab7
90 changed files with 8270 additions and 7378 deletions

View File

@ -7,6 +7,9 @@ omit =
homeassistant/external/* homeassistant/external/*
# omit pieces of code that rely on external devices being present # 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
homeassistant/components/*/wink.py homeassistant/components/*/wink.py
@ -32,13 +35,16 @@ omit =
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/notify/file.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py homeassistant/components/notify/pushover.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py

View File

@ -1,4 +1,4 @@
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) # Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](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/). 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 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 [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/) * 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) * 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 * 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

View File

@ -159,5 +159,5 @@ scene:
light.tv_back_light: on light.tv_back_light: on
light.ceiling: light.ceiling:
state: on state: on
color: [0.33, 0.66] xy_color: [0.33, 0.66]
brightness: 200 brightness: 200

View File

@ -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 lights are turned off
- turn it off if all people leave the house - turn it off if all people leave the house
- offer a service to turn it on for 10 seconds - 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 time
import logging import logging
@ -31,6 +47,7 @@ CONF_TARGET = 'target'
# Name of the service that we expose # Name of the service that we expose
SERVICE_FLASH = 'flash' SERVICE_FLASH = 'flash'
# Shortcut for the logger
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -3,6 +3,14 @@ custom_components.hello_world
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements the bare minimum that a component should implement. 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 # The domain of your component. Should be equal to the name of your component

View File

@ -186,6 +186,24 @@ def from_config_file(config_path, hass=None):
def enable_logging(hass): def enable_logging(hass):
""" Setup the logging for home assistant. """ """ Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO) 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 # Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path('home-assistant.log') err_log_path = hass.config.path('home-assistant.log')
@ -202,7 +220,7 @@ def enable_logging(hass):
err_handler.setLevel(logging.WARNING) err_handler.setLevel(logging.WARNING)
err_handler.setFormatter( err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s', 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) logging.getLogger('').addHandler(err_handler)
else: else:

View File

@ -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()

View File

@ -93,17 +93,17 @@ def setup(hass, config):
# Setup fake device tracker # Setup fake device tracker
hass.states.set("device_tracker.paulus", "home", hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE: {ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/schoutsen/picture"}) "http://graph.facebook.com/297400035/picture"})
hass.states.set("device_tracker.anne_therese", "not_home", hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_ENTITY_PICTURE: {ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/anne.t.frederiksen/picture"}) "http://graph.facebook.com/621994601/picture"})
hass.states.set("group.all_devices", "home", hass.states.set("group.all_devices", "home",
{ {
"auto": True, "auto": True,
ATTR_ENTITY_ID: [ ATTR_ENTITY_ID: [
"device_tracker.Paulus", "device_tracker.paulus",
"device_tracker.Anne_Therese" "device_tracker.anne_therese"
] ]
}) })

View File

@ -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

View File

@ -20,13 +20,21 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
_LOGGER = logging.getLogger(__name__) _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): def setup(hass, config):
""" Setup serving the frontend. """ """ Setup serving the frontend. """
if 'http' not in hass.config.components: if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded') _LOGGER.error('Dependency http is not loaded')
return False 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 # Static files
hass.http.register_path( hass.http.register_path(

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "24f15feebc48785ce908064dccbdb204" VERSION = "edce0feb9f77dd8b0bbe3c9b1e749fe0"

File diff suppressed because one or more lines are too long

View File

@ -31,12 +31,13 @@
"paper-slider": "PolymerElements/paper-slider#^1.0.0", "paper-slider": "PolymerElements/paper-slider#^1.0.0",
"paper-checkbox": "PolymerElements/paper-checkbox#^1.0.0", "paper-checkbox": "PolymerElements/paper-checkbox#^1.0.0",
"paper-drawer-panel": "PolymerElements/paper-drawer-panel#^1.0.0", "paper-drawer-panel": "PolymerElements/paper-drawer-panel#^1.0.0",
"paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#~1.0", "paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#^1.0.0",
"google-apis": "GoogleWebComponents/google-apis#0.8-preview", "google-apis": "GoogleWebComponents/google-apis#0.8-preview",
"moment": "^2.10.3", "moment": "^2.10.3",
"layout": "Polymer/layout", "layout": "Polymer/layout",
"color-picker-element": "~0.0.3", "paper-styles": "polymerelements/paper-styles#^1.0.0",
"paper-styles": "polymerelements/paper-styles#~1.0" "lodash": "~3.9.3",
"pikaday": "~1.3.2"
}, },
"resolutions": { "resolutions": {
"polymer": "^1.0.0", "polymer": "^1.0.0",

View File

@ -33,8 +33,8 @@
<div class='horizontal justified layout'> <div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info> <state-info state-obj="[[stateObj]]"></state-info>
<div class='state'> <div class='state'>
<div class='main-text'>[[computePrimaryText(stateObj)]]</div> <div class='main-text'>[[computePrimaryText(stateObj, isPlaying)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj)]]</div> <div class='secondary-text'>[[computeSecondaryText(stateObj, isPlaying)]]</div>
</div> </div>
</div> </div>
</template> </template>
@ -42,6 +42,7 @@
<script> <script>
(function() { (function() {
var PLAYING_STATES = ['playing', 'paused'];
Polymer({ Polymer({
is: 'state-card-media_player', is: 'state-card-media_player',
@ -49,14 +50,41 @@
stateObj: { stateObj: {
type: Object, type: Object,
}, },
isPlaying: {
type: Boolean,
computed: 'computeIsPlaying(stateObj)',
},
}, },
computePrimaryText: function(stateObj) { computeIsPlaying: function(stateObj) {
return stateObj.attributes.media_title || stateObj.stateDisplay; return PLAYING_STATES.indexOf(stateObj.state) !== -1;
}, },
computeSecondaryText: function(stateObj) { computePrimaryText: function(stateObj, isPlaying) {
return stateObj.attributes.media_title ? stateObj.stateDisplay : ''; return isPlaying ? stateObj.attributes.media_title : stateObj.stateDisplay;
},
computeSecondaryText: function(stateObj, isPlaying) {
var text;
if (stateObj.attributes.media_content_type == 'music') {
return stateObj.attributes.media_artist;
} else if (stateObj.attributes.media_content_type == 'tvshow') {
text = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season && stateObj.attributes.media_episode) {
text += ' S' + stateObj.attributes.media_season + 'E' + stateObj.attributes.media_episode;
}
return text;
} else if (stateObj.attributes.app_name) {
return stateObj.attributes.app_name;
} else {
return '';
}
}, },
}); });
})(); })();

View File

@ -5,10 +5,10 @@
<dom-module id="state-card-scene"> <dom-module id="state-card-scene">
<template> <template>
<template is='dom-if' if=[[allowToggle]]> <template is='dom-if' if='[[allowToggle]]'>
<state-card-toggle state-obj="[[stateObj]]"></state-card-toggle> <state-card-toggle state-obj="[[stateObj]]"></state-card-toggle>
</template> </template>
<template is='dom-if' if=[[!allowToggle]]> <template is='dom-if' if='[[!allowToggle]]'>
<state-card-display state-obj="[[stateObj]]"></state-card-display> <state-card-display state-obj="[[stateObj]]"></state-card-display>
</template> </template>
</template> </template>

View File

@ -15,6 +15,11 @@
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
overflow: hidden;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
} }
</style> </style>
@ -25,7 +30,7 @@
<script> <script>
(function(){ (function(){
var uiActions = window.hass.uiActions; var moreInfoActions = window.hass.moreInfoActions;
Polymer({ Polymer({
is: 'state-card', is: 'state-card',
@ -40,8 +45,10 @@
'tap': 'cardTapped', 'tap': 'cardTapped',
}, },
cardTapped: function() { cardTapped: function(ev) {
uiActions.showMoreInfoDialog(this.stateObj.entityId); ev.stopPropagation();
this.async(moreInfoActions.selectEntity.bind(
this, this.stateObj.entityId), 100);
}, },
}); });
})(); })();

View File

@ -20,7 +20,7 @@
<template> <template>
<ul> <ul>
<template is='dom-repeat' items='[[entities]]' as='entity'> <template is='dom-repeat' items='[[entities]]' as='entity'>
<li><a href='#' on-click='entitySelected'>[[entity]]</a></li> <li><a href='#' on-click='entitySelected'>[[entity.entityId]]</a></li>
</template> </template>
</ul> </ul>
</template> </template>
@ -28,25 +28,30 @@
<script> <script>
(function() { (function() {
var entityGetters = window.hass.entityGetters;
Polymer({ Polymer({
is: 'entity-list', is: 'entity-list',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
entities: { entities: {
type: Array, type: Array,
value: [], bindNuclear: [
entityGetters.entityMap,
function(map) {
return map.valueSeq().
sortBy(function(entity) { return entity.entityId; })
.toArray();
},
],
}, },
}, },
stateStoreChanged: function(stateStore) {
this.entities = stateStore.entityIDs.toArray();
},
entitySelected: function(ev) { entitySelected: function(ev) {
ev.preventDefault(); ev.preventDefault();
this.fire('entity-selected', {entityId: ev.model.entity}); this.fire('entity-selected', {entityId: ev.model.entity.entityId});
}, },
}); });
})(); })();

View File

@ -31,22 +31,27 @@
<script> <script>
(function() { (function() {
var eventGetters = window.hass.eventGetters;
Polymer({ Polymer({
is: 'events-list', is: 'events-list',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
events: { events: {
type: Array, type: Array,
value: [], bindNuclear: [
eventGetters.entityMap,
function(map) {
return map.valueSeq()
.sortBy(function(event) { return event.event; })
.toArray();
}
],
}, },
}, },
eventStoreChanged: function(eventStore) {
this.events = eventStore.all.toArray();
},
eventSelected: function(ev) { eventSelected: function(ev) {
ev.preventDefault(); ev.preventDefault();
this.fire('event-selected', {eventType: ev.model.event.event}); this.fire('event-selected', {eventType: ev.model.event.event});

View File

@ -0,0 +1,169 @@
<link rel='import' href='../bower_components/polymer/polymer.html'>
<dom-module id='ha-color-picker'>
<style>
canvas {
cursor: crosshair;
}
</style>
<template>
<canvas width='[[width]]' height='[[height]]'></canvas>
</template>
</dom-module>
<script>
/**
* Color-picker custom element
* Originally created by bbrewer97202 (Ben Brewer). MIT Licensed.
* https://github.com/bbrewer97202/color-picker-element
*
* Adapted to work with Polymer.
*/
(function() {
/**
* given red, green, blue values, return the equivalent hexidecimal value
* base source: http://stackoverflow.com/a/5624139
*/
var componentToHex = function(c) {
var hex = c.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
var rgbToHex = function(color) {
return "#" + componentToHex(color.r) + componentToHex(color.g) +
componentToHex(color.b);
};
Polymer({
is: 'ha-color-picker',
properties: {
width: {
type: Number,
value: 300,
},
height: {
type: Number,
value: 300,
},
color: {
type: Object,
},
},
listeners: {
'mousedown': 'onMouseDown',
'mouseup': 'onMouseUp',
'touchstart': 'onTouchStart',
'touchend': 'onTouchEnd',
'tap': 'onTap',
},
onMouseDown: function(e) {
this.onMouseMove(e);
this.addEventListener('mousemove', this.onMouseMove);
},
onMouseUp: function(e) {
this.removeEventListener('mousemove', this.onMouseMove);
},
onTouchStart: function(e) {
this.onTouchMove(e);
this.addEventListener('touchmove', this.onTouchMove);
},
onTouchEnd: function(e) {
this.removeEventListener('touchmove', this.onTouchMove);
},
onTap: function(e) {
e.stopPropagation();
},
onTouchMove: function(e) {
var touch = e.touches[0];
this.onColorSelect(e, {x: touch.clientX, y: touch.clientY});
},
onMouseMove: function(e) {
e.preventDefault();
if (this.mouseMoveIsThrottled) {
this.mouseMoveIsThrottled = false;
this.onColorSelect(e);
this.async(
function() { this.mouseMoveIsThrottled = true; }.bind(this), 100);
}
},
onColorSelect: function(e, coords) {
if (this.context) {
coords = coords || this.relativeMouseCoordinates(e);
var data = this.context.getImageData(coords.x, coords.y, 1, 1).data;
this.setColor({r: data[0], g: data[1], b: data[2]});
}
},
setColor: function(rgb) {
//save calculated color
this.color = {hex: rgbToHex(rgb), rgb: rgb};
this.fire('colorselected', {
rgb: this.color.rgb,
hex: this.color.hex
});
},
/**
* given a mouse click event, return x,y coordinates relative to the clicked target
* @returns object with x, y values
*/
relativeMouseCoordinates: function(e) {
var x = 0, y = 0;
if (this.canvas) {
var rect = this.canvas.getBoundingClientRect();
x = e.clientX - rect.left;
y = e.clientY - rect.top;
}
return {
x: x,
y: y
};
},
ready: function() {
this.setColor = this.setColor.bind(this);
this.mouseMoveIsThrottled = true;
this.canvas = this.children[0];
this.context = this.canvas.getContext('2d');
var colorGradient = this.context.createLinearGradient(0, 0, this.width, 0);
colorGradient.addColorStop(0, "rgb(255,0,0)");
colorGradient.addColorStop(0.16, "rgb(255,0,255)");
colorGradient.addColorStop(0.32, "rgb(0,0,255)");
colorGradient.addColorStop(0.48, "rgb(0,255,255)");
colorGradient.addColorStop(0.64, "rgb(0,255,0)");
colorGradient.addColorStop(0.80, "rgb(255,255,0)");
colorGradient.addColorStop(1, "rgb(255,0,0)");
this.context.fillStyle = colorGradient;
this.context.fillRect(0, 0, this.width, this.height);
var bwGradient = this.context.createLinearGradient(0, 0, 0, this.height);
bwGradient.addColorStop(0, "rgba(255,255,255,1)");
bwGradient.addColorStop(0.5, "rgba(255,255,255,0)");
bwGradient.addColorStop(0.5, "rgba(0,0,0,0)");
bwGradient.addColorStop(1, "rgba(0,0,0,1)");
this.context.fillStyle = bwGradient;
this.context.fillRect(0, 0, this.width, this.height);
},
});
})();
</script>

View File

@ -10,6 +10,9 @@
} }
</style> </style>
<template> <template>
<template is='dom-if' if='[[noEntries(entries)]]'>
No logbook entries found.
</template>
<template is='dom-repeat' items="[[entries]]"> <template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry> <logbook-entry entry-obj="[[item]]"></logbook-entry>
</template> </template>
@ -27,6 +30,10 @@
value: [], value: [],
}, },
}, },
noEntries: function(entries) {
return !entries.length;
}
}); });
})(); })();
</script> </script>

View File

@ -0,0 +1,247 @@
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/layout/layout.html'>
<link rel='import' href='../bower_components/paper-header-panel/paper-header-panel.html'>
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
<!--
Too broken for now.
<link rel='import' href='../bower_components/paper-menu/paper-menu.html'> -->
<link rel='import' href='../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../bower_components/paper-item/paper-icon-item.html'>
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel='import' href='../components/stream-status.html'>
<dom-module id='ha-sidebar'>
<style>
.sidenav {
background: #fafafa;
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
overflow: hidden;
white-space: nowrap;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
/*.sidenav paper-menu {
--paper-menu-color: var(--secondary-text-color);
--paper-menu-background-color: #fafafa;
}*/
div.menu {
color: var(--secondary-text-color);
background-color: #fafafa;
}
paper-icon-item {
cursor: pointer;
}
paper-icon-item.selected {
font-weight: bold;
}
paper-icon-item.logout {
margin-top: 16px;
}
.divider {
border-top: 1px solid #e0e0e0;
}
.text {
padding: 16px;
font-size: 14px;
}
.dev-tools {
padding: 0 8px;
}
</style>
<template>
<paper-header-panel mode='scroll' class='sidenav fit'>
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div class="title">Home Assistant</div>
</paper-toolbar>
<!-- <paper-menu id='menu' selected='{{menuSelected}}'
selectable='[data-panel]' attr-for-selected='data-panel'> -->
<div class='menu'>
<paper-icon-item on-click='menuClicked' data-panel='states'>
<iron-icon item-icon icon='apps'></iron-icon> States
</paper-icon-item>
<template is='dom-repeat' items='{{possibleFilters}}'>
<paper-icon-item on-click='menuClicked' data-panel$='[[filterType(item)]]'>
<iron-icon item-icon icon='[[filterIcon(item)]]'></iron-icon>
<span>[[filterName(item)]]</span>
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasHistoryComponent]]'>
<paper-icon-item on-click='menuClicked' data-panel='history'>
<iron-icon item-icon icon='assessment'></iron-icon>
History
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasLogbookComponent]]'>
<paper-icon-item on-click='menuClicked' data-panel='logbook'>
<iron-icon item-icon icon='list'></iron-icon>
Logbook
</paper-icon-item>
</template>
<paper-icon-item on-click='menuClicked' data-panel='logout' class='logout'>
<iron-icon item-icon icon='exit-to-app'></iron-icon>
Log Out
</paper-icon-item>
<paper-item class='divider horizontal layout justified'>
<div>Streaming updates</div>
<stream-status></stream-status>
</paper-item>
<div class='text label divider'>Developer Tools</div>
<div class='dev-tools layout horizontal justified'>
<paper-icon-button
icon='settings-remote' data-panel='devService'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-ethernet' data-panel='devState'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-input-antenna' data-panel='devEvent'
on-click='handleDevClick'></paper-icon-button>
</div>
<!-- </paper-menu> -->
</div>
</paper-header-panel>
</template>
</dom-module>
<script>
(function() {
var configGetters = window.hass.configGetters;
var navigationGetters = window.hass.navigationGetters;
var authActions = window.hass.authActions;
var navigationActions = window.hass.navigationActions;
var uiUtil = window.hass.uiUtil;
var entityDomainFilters = window.hass.util.entityDomainFilters;
Polymer({
is: 'ha-sidebar',
behaviors: [nuclearObserver],
properties: {
menuSelected: {
type: String,
// observer: 'menuSelectedChanged',
},
selected: {
type: String,
bindNuclear: navigationGetters.activePage,
observer: 'selectedChanged',
},
possibleFilters: {
type: Array,
value: [],
bindNuclear: [
navigationGetters.possibleEntityDomainFilters,
function(domains) { return domains.toArray(); }
],
},
hasHistoryComponent: {
type: Boolean,
bindNuclear: configGetters.isComponentLoaded('history'),
},
hasLogbookComponent: {
type: Boolean,
bindNuclear: configGetters.isComponentLoaded('logbook'),
},
},
// menuSelectedChanged: function(newVal) {
// if (this.selected !== newVal) {
// this.selectPanel(newVal);
// }
// },
selectedChanged: function(newVal) {
// if (this.menuSelected !== newVal) {
// this.menuSelected = newVal;
// }
var menuItems = this.querySelectorAll('.menu [data-panel]');
for (var i = 0; i < menuItems.length; i++) {
if(menuItems[i].dataset.panel === newVal) {
menuItems[i].classList.add('selected');
} else {
menuItems[i].classList.remove('selected');
}
}
},
menuClicked: function(ev) {
var target = ev.target;
var checks = 5;
// find panel to select
while(checks && !target.dataset.panel) {
target = target.parentElement;
checks--;
}
if (checks) {
this.selectPanel(target.dataset.panel);
}
},
handleDevClick: function(ev) {
// prevent it from highlighting first menu item
document.activeElement.blur();
this.menuClicked(ev);
},
selectPanel: function(newChoice) {
if(newChoice === this.selected) {
return;
} else if (newChoice == 'logout') {
this.handleLogOut();
return;
}
navigationActions.navigate.apply(null, newChoice.split('/'));
},
handleLogOut: function() {
authActions.logOut();
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return entityDomainFilters[filter];
},
filterType: function(filter) {
return 'states/' + filter;
},
});
})();
</script>

View File

@ -0,0 +1,61 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<dom-module id="ha-voice-command-progress">
<style>
:host {
display: block;
}
iron-icon {
margin-right: 8px;
}
.interimTranscript {
color: darkgrey;
}
.listening paper-spinner {
float: right;
}
</style>
<template>
<iron-icon icon="av:hearing"></iron-icon>
<span>{{finalTranscript}}</span>
<span class='interimTranscript'>[[interimTranscript]]</span>
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
</template>
</dom-module>
<script>
(function(){
var voiceGetters = window.hass.voiceGetters;
Polymer({
is: 'ha-voice-command-progress',
behaviors: [nuclearObserver],
properties: {
isTransmitting: {
type: Boolean,
bindNuclear: voiceGetters.isTransmitting,
},
interimTranscript: {
type: String,
bindNuclear: voiceGetters.extraInterimTranscript,
},
finalTranscript: {
type: String,
bindNuclear: voiceGetters.finalTranscript,
},
},
});
})();
</script>

View File

@ -54,14 +54,14 @@
<script> <script>
(function() { (function() {
var uiActions = window.hass.uiActions; var moreInfoActions = window.hass.moreInfoActions;
Polymer({ Polymer({
is: 'logbook-entry', is: 'logbook-entry',
entityClicked: function(ev) { entityClicked: function(ev) {
ev.preventDefault(); ev.preventDefault();
uiActions.showMoreInfoDialog(this.entryObj.entityId); moreInfoActions.selectEntity(this.entryObj.entityId);
} }
}); });

View File

@ -23,10 +23,10 @@
<template> <template>
<ul> <ul>
<template is='dom-repeat' items="[[domains]]" as="domain"> <template is='dom-repeat' items="[[serviceDomains]]" as="domain">
<template is='dom-repeat' items="[[computeServices(domain)]]" as="service"> <template is='dom-repeat' items="[[domain.services]]" as="service">
<li><a href='#' on-click='serviceClicked'> <li><a href='#' on-click='serviceClicked'>
<span>[[domain]]</span>/<span>[[service]]</span> <span>[[domain.domain]]</span>/<span>[[service]]</span>
</a></li> </a></li>
</template> </template>
</template> </template>
@ -36,19 +36,24 @@
<script> <script>
(function() { (function() {
var serviceGetters = window.hass.serviceGetters;
Polymer({ Polymer({
is: 'services-list', is: 'services-list',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
domains: { serviceDomains: {
type: Array, type: Array,
value: [], bindNuclear: [
}, serviceGetters.entityMap,
function(map) {
services: { return map.valueSeq()
type: Object, .sortBy(function(domain) { return domain.domain; })
.toJS();
},
],
}, },
}, },
@ -56,15 +61,10 @@
return this.services.get(domain).toArray(); return this.services.get(domain).toArray();
}, },
serviceStoreChanged: function(serviceStore) {
this.services = serviceStore.all;
this.domains = this.services.keySeq().sort().toArray();
},
serviceClicked: function(ev) { serviceClicked: function(ev) {
ev.preventDefault(); ev.preventDefault();
this.fire( this.fire(
'service-selected', {domain: ev.model.domain, service: ev.model.service}); 'service-selected', {domain: ev.model.domain.domain, service: ev.model.service});
}, },
}); });
})(); })();

View File

@ -32,8 +32,8 @@
background-color: #fff; background-color: #fff;
border-radius: 2px; border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px; box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
padding: 16px; padding: 0 16px 8px;
margin: 16px auto; margin: 16px;
} }
</style> </style>
@ -44,9 +44,15 @@
<state-card class="state-card" state-obj="[[item]]"></state-card> <state-card class="state-card" state-obj="[[item]]"></state-card>
</template> </template>
<template if="[[computeEmptyStates(states)]]"> <template is='dom-if' if="[[computeEmptyStates(states)]]">
<div class='no-states-content'> <div class='no-states-content'>
<content></content> <h3>Hi there!</h3>
<p>
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
</p>
<p>
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
</p>
</div> </div>
</template> </template>

View File

@ -1,5 +1,7 @@
<link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/lodash.html">
<script> <script>
(function() { (function() {
Polymer({ Polymer({

View File

@ -1,5 +1,14 @@
<link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module is='state-history-chart-timeline'>
<style>
:host {
display: block;
}
</style>
<template></template>
</dom-module>
<script> <script>
(function() { (function() {
Polymer({ Polymer({
@ -18,10 +27,6 @@
}, },
}, },
created: function() {
this.style.display = 'block';
},
attached: function() { attached: function() {
this.isAttached = true; this.isAttached = true;
}, },
@ -34,18 +39,17 @@
if (!this.isAttached) { if (!this.isAttached) {
return; return;
} }
var root = Polymer.dom(this); var root = Polymer.dom(this);
var stateHistory = this.data; var stateHistory = this.data;
while (root.lastChild) { while (root.node.lastChild) {
root.removeChild(root.lastChild); root.node.removeChild(root.node.lastChild);
} }
if (!stateHistory || stateHistory.length === 0) { if (!stateHistory || stateHistory.length === 0) {
return; return;
} }
// debugger;
var chart = new google.visualization.Timeline(this); var chart = new google.visualization.Timeline(this);
var dataTable = new google.visualization.DataTable(); var dataTable = new google.visualization.DataTable();
@ -59,14 +63,19 @@
dataTable.addRow([entityDisplay, stateStr, start, end]); dataTable.addRow([entityDisplay, stateStr, start, end]);
}; };
// people can pass in history of 1 entityId or a collection. var startTime = new Date(
// var stateHistory; stateHistory.reduce(function(minTime, stateInfo) {
// if (_.isArray(data[0])) { return Math.min(
// stateHistory = data; minTime, stateInfo[0].lastChangedAsDate);
// } else { }, new Date())
// stateHistory = [data]; );
// isSingleDevice = true;
// } // end time is Math.min(curTime, start time + 1 day)
var endTime = new Date(startTime);
endTime.setDate(endTime.getDate()+1);
if (endTime > new Date()) {
endTime = new Date();
}
var numTimelines = 0; var numTimelines = 0;
// stateHistory is a list of lists of sorted state objects // stateHistory is a list of lists of sorted state objects
@ -90,17 +99,13 @@
} }
}); });
addRow(entityDisplay, prevState, prevLastChanged, new Date()); addRow(entityDisplay, prevState, prevLastChanged, endTime);
numTimelines++; numTimelines++;
}.bind(this)); }.bind(this));
chart.draw(dataTable, { chart.draw(dataTable, {
height: 55 + numTimelines * 42, height: 55 + numTimelines * 42,
// interactive properties require CSS, the JS api puts it on the document
// instead of inside our Shadow DOM.
enableInteractivity: false,
timeline: { timeline: {
showRowLabels: stateHistory.length > 1 showRowLabels: stateHistory.length > 1
}, },

View File

@ -16,29 +16,35 @@
text-align: center; text-align: center;
padding: 8px; padding: 8px;
} }
.loading {
height: 0px;
overflow: hidden;
}
</style> </style>
<template> <template>
<google-legacy-loader on-api-load="googleApiLoaded"></google-legacy-loader> <google-legacy-loader on-api-load="googleApiLoaded"></google-legacy-loader>
<div hidden$="{{!isLoading}}" class='loading-container'> <div hidden$="{{!isLoading}}" class='loading-container'>
<loading-box>Loading history data</loading-box> <loading-box>Updating history data</loading-box>
</div> </div>
<template is='dom-if' if='[[!isLoading]]'> <div class$='[[computeContentClasses(isLoading)]]'>
<template is='dom-if' if='[[groupedStateHistory.timeline]]'> <template is='dom-if' if='[[computeIsEmpty(stateHistory)]]'>
<state-history-chart-timeline data='[[groupedStateHistory.timeline]]' No state history found.
is-single-device='[[isSingleDevice]]'>
</state-history-chart-timeline>
</template> </template>
<template is='dom-if' if='[[groupedStateHistory.line]]'> <state-history-chart-timeline
<template is='dom-repeat' items='[[groupedStateHistory.line]]'> data='[[groupedStateHistory.timeline]]'
<state-history-chart-line unit='[[extractUnit(item)]]' is-single-device='[[isSingleDevice]]'>
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'> </state-history-chart-timeline>
</state-history-chart-line>
</template> <template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<state-history-chart-line unit='[[extractUnit(item)]]'
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'>
</state-history-chart-line>
</template> </template>
</template> </div>
</template> </template>
</dom-module> </dom-module>
@ -69,7 +75,7 @@
groupedStateHistory: { groupedStateHistory: {
type: Object, type: Object,
computed: 'computeGroupedStateHistory(stateHistory)', computed: 'computeGroupedStateHistory(isLoading, stateHistory)',
}, },
isSingleDevice: { isSingleDevice: {
@ -79,36 +85,35 @@
}, },
computeIsSingleDevice: function(stateHistory) { computeIsSingleDevice: function(stateHistory) {
return stateHistory && stateHistory.length == 1; return stateHistory && stateHistory.size == 1;
}, },
computeGroupedStateHistory: function(stateHistory) { computeGroupedStateHistory: function(isLoading, stateHistory) {
if (isLoading || !stateHistory) {
return {line: [], timeline: []};
}
var lineChartDevices = {}; var lineChartDevices = {};
var timelineDevices = []; var timelineDevices = [];
if (!stateHistory) {
return {line: unitStates, timeline: timelineDevices};
}
stateHistory.forEach(function(stateInfo) { stateHistory.forEach(function(stateInfo) {
if (!stateInfo || stateInfo.length === 0) { if (!stateInfo || stateInfo.size === 0) {
return; return;
} }
var unit; var stateWithUnit = stateInfo.find(function(state) {
return 'unit_of_measurement' in state.attributes;
});
for (var i = 0; i < stateInfo.length && !unit; i++) { var unit = stateWithUnit ?
unit = stateInfo[i].attributes.unit_of_measurement; stateWithUnit.attributes.unit_of_measurement : false;
}
if (unit) { if (!unit) {
if (!(unit in lineChartDevices)) { timelineDevices.push(stateInfo.toArray());
lineChartDevices[unit] = [stateInfo]; } else if(unit in lineChartDevices) {
} else { lineChartDevices[unit].push(stateInfo.toArray());
lineChartDevices[unit].push(stateInfo);
}
} else { } else {
timelineDevices.push(stateInfo); lineChartDevices[unit] = [stateInfo.toArray()];
} }
}); });
@ -129,10 +134,18 @@
}); });
}, },
computeContentClasses: function(isLoading) {
return isLoading ? 'loading' : '';
},
computeIsLoading: function(isLoadingData, apiLoaded) { computeIsLoading: function(isLoadingData, apiLoaded) {
return isLoadingData || !apiLoaded; return isLoadingData || !apiLoaded;
}, },
computeIsEmpty: function(stateHistory) {
return stateHistory && stateHistory.size === 0;
},
extractUnit: function(arr) { extractUnit: function(arr) {
return arr[0]; return arr[0];
}, },

View File

@ -17,42 +17,37 @@
} }
</style> </style>
<template> <template>
<iron-icon icon="warning" hidden$="{{!hasError}}"></iron-icon> <iron-icon icon="warning" hidden$="[[!hasError]]"></iron-icon>
<paper-toggle-button id="toggle" on-change='toggleChanged' hidden$="{{hasError}}"></paper-toggle-button> <paper-toggle-button id="toggle" on-change='toggleChanged' checked$='[[isStreaming]]' hidden$="[[hasError]]"></paper-toggle-button>
</template> </template>
</dom-module> </dom-module>
<script> <script>
var streamGetters = window.hass.streamGetters;
var streamActions = window.hass.streamActions; var streamActions = window.hass.streamActions;
var authStore = window.hass.authStore;
Polymer({ Polymer({
is: 'stream-status', is: 'stream-status',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
isStreaming: { isStreaming: {
type: Boolean, type: Boolean,
value: false, bindNuclear: streamGetters.isStreamingEvents,
}, },
hasError: { hasError: {
type: Boolean, type: Boolean,
value: false, bindNuclear: streamGetters.hasStreamingEventsError,
}, },
}, },
streamStoreChanged: function(streamStore) { toggleChanged: function() {
this.hasError = streamStore.hasError;
this.$.toggle.checked = this.isStreaming = streamStore.isStreaming;
},
toggleChanged: function(ev) {
if (this.isStreaming) { if (this.isStreaming) {
streamActions.stop(); streamActions.stop();
} else { } else {
streamActions.start(authStore.authToken); streamActions.start();
} }
}, },
}); });

View File

@ -31,7 +31,7 @@
</style> </style>
<template> <template>
<!-- entry-animation='slide-up-animation' exit-animation='slide-down-animation' --> <!-- entry-animation='slide-up-animation' exit-animation='slide-down-animation' -->
<paper-dialog id="dialog" with-backdrop> <paper-dialog id="dialog" with-backdrop opened='{{dialogOpen}}'>
<h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2> <h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2>
<div> <div>
<template is='dom-if' if="[[showHistoryComponent]]"> <template is='dom-if' if="[[showHistoryComponent]]">
@ -49,9 +49,13 @@
<script> <script>
(function() { (function() {
var stateStore = window.hass.stateStore;
var stateHistoryStore = window.hass.stateHistoryStore; var configGetters = window.hass.configGetters;
var stateHistoryActions = window.hass.stateHistoryActions; var entityHistoryGetters = window.hass.entityHistoryGetters;
var entityHistoryActions = window.hass.entityHistoryActions;
var moreInfoGetters = window.hass.moreInfoGetters;
var moreInfoActions = window.hass.moreInfoActions;
// if you don't want the history component to show add the domain to this array // if you don't want the history component to show add the domain to this array
var DOMAINS_WITH_NO_HISTORY = ['camera']; var DOMAINS_WITH_NO_HISTORY = ['camera'];
@ -59,29 +63,40 @@
Polymer({ Polymer({
is: 'more-info-dialog', is: 'more-info-dialog',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
entityId: {
type: String,
},
stateObj: { stateObj: {
type: Object, type: Object,
bindNuclear: moreInfoGetters.currentEntity,
observer: 'stateObjChanged',
}, },
stateHistory: { stateHistory: {
type: Object, type: Object,
bindNuclear: [
moreInfoGetters.currentEntityHistory,
function(history) {
return history ? [history] : false;
},
],
}, },
isLoadingHistoryData: { isLoadingHistoryData: {
type: Boolean, type: Boolean,
value: false, bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
}, },
hasHistoryComponent: { hasHistoryComponent: {
type: Boolean, type: Boolean,
value: false, bindNuclear: configGetters.isComponentLoaded('history'),
observer: 'fetchHistoryData',
},
shouldFetchHistory: {
type: Boolean,
bindNuclear: moreInfoGetters.isCurrentEntityHistoryStale,
observer: 'fetchHistoryData',
}, },
showHistoryComponent: { showHistoryComponent: {
@ -92,23 +107,14 @@
dialogOpen: { dialogOpen: {
type: Boolean, type: Boolean,
value: false, value: false,
observer: 'dialogOpenChanged',
}, },
}, },
listeners: { fetchHistoryData: function() {
'iron-overlay-opened': 'onIronOverlayOpened', if (this.stateObj && this.hasHistoryComponent &&
'iron-overlay-closed': 'onIronOverlayClosed' this.shouldFetchHistory) {
}, entityHistoryActions.fetchRecent(this.stateObj.entityId);
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
stateStoreChanged: function() {
var newState = this.entityId ? stateStore.get(this.entityId) : null;
if (newState !== this.stateObj) {
this.stateObj = newState;
} }
if(this.stateObj) { if(this.stateObj) {
if(DOMAINS_WITH_NO_HISTORY.indexOf(this.stateObj.domain) !== -1) { if(DOMAINS_WITH_NO_HISTORY.indexOf(this.stateObj.domain) !== -1) {
@ -120,49 +126,26 @@
} }
}, },
stateHistoryStoreChanged: function() { stateObjChanged: function(newVal) {
var newHistory; if (!newVal) {
this.dialogOpen = false;
if (this.hasHistoryComponent && this.entityId) { return;
newHistory = [stateHistoryStore.get(this.entityId)];
} else {
newHistory = null;
} }
this.isLoadingHistoryData = false; this.fetchHistoryData();
if (newHistory !== this.stateHistory) { // allow dialog to render content before showing it so it is
this.stateHistory = newHistory; // positioned correctly.
this.async(function() {
this.dialogOpen = true;
}.bind(this), 10);
},
dialogOpenChanged: function(newVal) {
if (!newVal) {
moreInfoActions.deselectEntity();
} }
}, },
onIronOverlayOpened: function() {
this.dialogOpen = true;
},
onIronOverlayClosed: function() {
this.dialogOpen = false;
},
changeEntityId: function(entityId) {
this.entityId = entityId;
this.stateStoreChanged();
this.stateHistoryStoreChanged();
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
this.isLoadingHistoryData = true;
stateHistoryActions.fetch(entityId);
}
},
show: function(entityId) {
this.changeEntityId(entityId);
this.debounce('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
}); });
})(); })();
</script> </script>

@ -1 +1 @@
Subproject commit 015edf9c28a63122aa8f6bc153f0c0ddfaad1caa Subproject commit 14f2bb779eb165bce236dcdc69d83e08ab73da1c

View File

@ -21,9 +21,8 @@
} }
</style> </style>
<home-assistant-icons></home-assistant-icons>
<template> <template>
<home-assistant-icons></home-assistant-icons>
<template is='dom-if' if='[[!loaded]]'> <template is='dom-if' if='[[!loaded]]'>
<login-form></login-form> <login-form></login-form>
</template> </template>
@ -37,10 +36,9 @@
<script> <script>
(function() { (function() {
var uiActions = window.hass.uiActions;
var storeListenerMixIn = window.hass.storeListenerMixIn, var syncGetters = window.hass.syncGetters;
uiActions = window.hass.uiActions, var preferences = window.hass.localStoragePreferences;
preferenceStore = window.hass.preferenceStore;
Polymer({ Polymer({
is: 'home-assistant', is: 'home-assistant',
@ -49,12 +47,15 @@
auth: null, auth: null,
}, },
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
auth: {
type: String,
},
loaded: { loaded: {
type: Boolean, type: Boolean,
value: false, bindNuclear: syncGetters.isDataLoaded,
}, },
}, },
@ -65,13 +66,11 @@
// if auth was given, tell the backend // if auth was given, tell the backend
if(this.auth) { if(this.auth) {
uiActions.validateAuth(this.auth, false); uiActions.validateAuth(this.auth, false);
} else if (preferenceStore.hasAuthToken) { } else if (preferences.authToken) {
uiActions.validateAuth(preferenceStore.authToken, false); uiActions.validateAuth(preferences.authToken, true);
} }
},
syncStoreChanged: function(syncStore) { preferences.startSync();
this.loaded = syncStore.initialLoadDone;
}, },
}); });

View File

@ -2,13 +2,6 @@
<link rel='import' href='../bower_components/layout/layout.html'> <link rel='import' href='../bower_components/layout/layout.html'>
<link rel='import' href='../bower_components/paper-drawer-panel/paper-drawer-panel.html'> <link rel='import' href='../bower_components/paper-drawer-panel/paper-drawer-panel.html'>
<link rel='import' href='../bower_components/paper-header-panel/paper-header-panel.html'>
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
<link rel='import' href='../bower_components/paper-menu/paper-menu.html'>
<link rel='import' href='../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../bower_components/paper-item/paper-icon-item.html'>
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel='import' href='../layouts/partial-states.html'> <link rel='import' href='../layouts/partial-states.html'>
<link rel='import' href='../layouts/partial-logbook.html'> <link rel='import' href='../layouts/partial-logbook.html'>
@ -18,115 +11,22 @@
<link rel='import' href='../layouts/partial-dev-set-state.html'> <link rel='import' href='../layouts/partial-dev-set-state.html'>
<link rel='import' href='../managers/notification-manager.html'> <link rel='import' href='../managers/notification-manager.html'>
<link rel='import' href='../managers/modal-manager.html'> <link rel="import" href="../dialogs/more-info-dialog.html">
<link rel='import' href='../components/stream-status.html'> <link rel='import' href='../components/ha-sidebar.html'>
<dom-module id='home-assistant-main'> <dom-module id='home-assistant-main'>
<style>
.sidenav {
background: #fafafa;
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.sidenav paper-menu {
--paper-menu-color: var(--secondary-text-color);
--paper-menu-background-color: #fafafa;
}
paper-icon-item {
cursor: pointer;
}
.divider {
border-top: 1px solid #e0e0e0;
}
.text {
padding: 16px;
font-size: 14px;
}
.dev-tools {
padding: 0 8px;
}
</style>
<template> <template>
<notification-manager></notification-manager> <notification-manager></notification-manager>
<modal-manager></modal-manager> <more-info-dialog></more-info-dialog>
<paper-drawer-panel id='drawer' narrow='{{narrow}}'> <paper-drawer-panel id='drawer' narrow='{{narrow}}'>
<paper-header-panel mode='scroll' drawer class='sidenav fit'> <ha-sidebar drawer></ha-sidebar>
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div title>Home Assistant</div>
</paper-toolbar>
<paper-menu id='menu' class='layout vertical fit' <template is='dom-if' if='[[isSelectedStates]]'>
selectable='[data-panel]' attr-for-selected='data-panel' <partial-states main narrow='[[narrow]]'>
on-iron-select='menuSelect' selected='[[selected]]'>
<paper-icon-item data-panel='states'>
<iron-icon item-icon icon='apps'></iron-icon> States
</paper-icon-item>
<template is='dom-repeat' items='{{activeFilters}}'>
<paper-icon-item data-panel$='[[filterType(item)]]'>
<iron-icon item-icon icon='[[filterIcon(item)]]'></iron-icon>
<span>[[filterName(item)]]</span>
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasHistoryComponent]]'>
<paper-icon-item data-panel='history'>
<iron-icon item-icon icon='assessment'></iron-icon>
History
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasLogbookComponent]]'>
<paper-icon-item data-panel='logbook'>
<iron-icon item-icon icon='list'></iron-icon>
Logbook
</paper-icon-item>
</template>
<div class='flex'></div>
<paper-icon-item data-panel='logout'>
<iron-icon item-icon icon='exit-to-app'></iron-icon>
Log Out
</paper-icon-item>
<paper-item class='divider horizontal layout justified'>
<div>Streaming updates</div>
<stream-status></stream-status>
</paper-item>
<div class='text label divider'>Developer Tools</div>
<div class='dev-tools layout horizontal justified'>
<paper-icon-button
icon='settings-remote' data-panel$='[[selectedDevService]]'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-ethernet' data-panel$='[[selectedDevState]]'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-input-antenna' data-panel$='[[selectedDevEvent]]'
on-click='handleDevClick'></paper-icon-button>
</div>
</paper-menu>
</paper-header-panel>
<template is='dom-if' if='[[!hideStates]]'>
<partial-states
main narrow='[[narrow]]'
filter='[[stateFilter]]'>
</partial-states> </partial-states>
</template> </template>
<template is='dom-if' if='[[isSelectedLogbook]]'> <template is='dom-if' if='[[isSelectedLogbook]]'>
<partial-logbook main narrow='[[narrow]]'></partial-logbook> <partial-logbook main narrow='[[narrow]]'></partial-logbook>
</template> </template>
@ -149,192 +49,83 @@
<script> <script>
(function() { (function() {
var configGetters = window.hass.configGetters;
var entityGetters = window.hass.entityGetters;
var navigationGetters = window.hass.navigationGetters;
var authActions = window.hass.authActions; var authActions = window.hass.authActions;
var navigationActions = window.hass.navigationActions;
var uiUtil = window.hass.uiUtil; var uiUtil = window.hass.uiUtil;
var uiConstants = window.hass.uiConstants; var entityDomainFilters = window.hass.util.entityDomainFilters;
var urlSync = window.hass.urlSync;
Polymer({ Polymer({
is: 'home-assistant-main', is: 'home-assistant-main',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
selected: {
type: String,
value: 'states',
},
stateFilter: {
type: String,
value: null,
},
narrow: { narrow: {
type: Boolean, type: Boolean,
}, },
activeFilters: { activePage: {
type: Array,
value: [],
},
hasHistoryComponent: {
type: Boolean,
value: false,
},
hasLogbookComponent: {
type: Boolean,
value: false,
},
isStreaming: {
type: Boolean,
value: false,
},
hasStreamError: {
type: Boolean,
value: false,
},
hideStates: {
type: Boolean,
value: false,
},
selectedHistory: {
type: String, type: String,
value: 'history', bindNuclear: navigationGetters.activePage,
readOnly: true, observer: 'activePageChanged',
},
isSelectedStates: {
type: Boolean,
bindNuclear: navigationGetters.isActivePane('states'),
}, },
isSelectedHistory: { isSelectedHistory: {
type: Boolean, type: Boolean,
computed: 'computeIsSelected(selected, selectedHistory)', bindNuclear: navigationGetters.isActivePane('history'),
},
selectedLogbook: {
type: String,
value: 'logbook',
readOnly: true,
}, },
isSelectedLogbook: { isSelectedLogbook: {
type: Boolean, type: Boolean,
computed: 'computeIsSelected(selected, selectedLogbook)', bindNuclear: navigationGetters.isActivePane('logbook'),
},
selectedDevEvent: {
type: String,
value: 'devEvent',
readOnly: true,
}, },
isSelectedDevEvent: { isSelectedDevEvent: {
type: Boolean, type: Boolean,
computed: 'computeIsSelected(selected, selectedDevEvent)', bindNuclear: navigationGetters.isActivePane('devEvent'),
},
selectedDevState: {
type: String,
value: 'devState',
readOnly: true,
}, },
isSelectedDevState: { isSelectedDevState: {
type: Boolean, type: Boolean,
computed: 'computeIsSelected(selected, selectedDevState)', bindNuclear: navigationGetters.isActivePane('devState'),
},
selectedDevService: {
type: String,
value: 'devService',
readOnly: true,
}, },
isSelectedDevService: { isSelectedDevService: {
type: Boolean, type: Boolean,
computed: 'computeIsSelected(selected, selectedDevService)', bindNuclear: navigationGetters.isActivePane('devService'),
}, },
}, },
listeners: { listeners: {
'menu.core-select': 'menuSelect',
'open-menu': 'openDrawer', 'open-menu': 'openDrawer',
}, },
stateStoreChanged: function(stateStore) {
this.activeFilters = stateStore.domains.filter(function(domain) {
return domain in uiConstants.STATE_FILTERS;
}).toArray();
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasLogbookComponent = componentStore.isLoaded('logbook');
},
menuSelect: function(ev, detail, sender) {
this.selectPanel(this.$.menu.selected);
},
handleDevClick: function(ev, detail, sender) {
// prevent it from highlighting first menu item
document.activeElement.blur();
this.selectPanel(ev.target.parentElement.dataset.panel);
},
selectPanel: function(newChoice) {
if (newChoice == 'logout') {
this.handleLogOut();
return;
} else if(newChoice == this.selected) {
return;
}
this.closeDrawer();
this.selected = newChoice;
if (newChoice.substr(0, 7) === 'states_') {
this.hideStates = false;
this.stateFilter = newChoice.substr(7);
} else {
this.hideStates = newChoice !== 'states';
this.stateFilter = null;
}
},
openDrawer: function() { openDrawer: function() {
this.$.drawer.openDrawer(); this.$.drawer.openDrawer();
}, },
closeDrawer: function() { activePageChanged: function() {
this.$.drawer.closeDrawer(); this.$.drawer.closeDrawer();
}, },
handleLogOut: function() { attached: function() {
authActions.logOut(); urlSync.startSync();
}, },
computeIsSelected: function(selected, selectedType) { detached: function() {
return selected === selectedType; urlSync.stopSync();
}, },
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return uiConstants.STATE_FILTERS[filter];
},
filterType: function(filter) {
return 'states_' + filter;
}
}); });
})(); })();
</script> </script>

View File

@ -13,6 +13,10 @@
<dom-module id="login-form"> <dom-module id="login-form">
<style> <style>
:host {
white-space: nowrap;
}
#passwordDecorator { #passwordDecorator {
display: block; display: block;
height: 57px; height: 57px;
@ -86,53 +90,50 @@
<script> <script>
(function() { (function() {
var uiActions = window.hass.uiActions; var uiActions = window.hass.uiActions;
var authGetters = window.hass.authGetters;
Polymer({ Polymer({
is: 'login-form', is: 'login-form',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
isValidating: { isValidating: {
type: Boolean, type: Boolean,
value: false, observer: 'isValidatingChanged',
bindNuclear: authGetters.isValidating,
}, },
isInvalid: { isInvalid: {
type: Boolean, type: Boolean,
value: false, bindNuclear: authGetters.isInvalidAttempt,
}, },
errorMessage: { errorMessage: {
type: String, type: String,
value: '', bindNuclear: authGetters.attemptErrorMessage,
} },
}, },
listeners: { listeners: {
'passwordInput.keydown': 'passwordKeyDown', 'keydown': 'passwordKeyDown',
'loginButton.click': 'validatePassword', 'loginButton.click': 'validatePassword',
}, },
attached: function() { observers: [
this.focusPassword(); 'validatingChanged(isValidating, isInvalid)',
}, ],
authStoreChanged: function(authStore) { validatingChanged: function(isValidating, isInvalid) {
this.isValidating = authStore.isValidating; if (!isValidating && !isInvalid) {
this.$.passwordInput.value = '';
if (authStore.lastAttemptInvalid) {
this.errorMessage = authStore.lastAttemptMessage;
this.isInvalid = true;
}
if (!this.isValidating) {
setTimeout(this.focusPassword.bind(this), 0);
} }
}, },
focusPassword: function() { isValidatingChanged: function(newVal) {
this.$.passwordInput.focus(); if (!newVal) {
this.async(function() { this.$.passwordInput.focus(); }.bind(this), 10);
}
}, },
passwordKeyDown: function(ev) { passwordKeyDown: function(ev) {

View File

@ -6,11 +6,18 @@
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'> <link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<dom-module id='partial-base'> <dom-module id='partial-base'>
<style>
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
</style>
<template> <template>
<paper-scroll-header-panel class='fit'> <paper-scroll-header-panel class='fit'>
<paper-toolbar> <paper-toolbar>
<paper-icon-button icon='menu' hidden$='[[!narrow]]' on-click='toggleMenu'></paper-icon-button> <paper-icon-button icon='menu' hidden$='[[!narrow]]' on-click='toggleMenu'></paper-icon-button>
<div title> <div class="title">
<content select='[header-title]'></content> <content select='[header-title]'></content>
</div> </div>
<content select='[header-buttons]'></content> <content select='[header-buttons]'></content>

View File

@ -80,7 +80,7 @@
return; return;
} }
eventActions.fire(this.eventType, eventData); eventActions.fireEvent(this.eventType, eventData);
}, },
computeFormClasses: function(narrow) { computeFormClasses: function(narrow) {

View File

@ -50,8 +50,9 @@
<script> <script>
(function() { (function() {
var stateStore = window.hass.stateStore; var reactor = window.hass.reactor;
var stateActions = window.hass.stateActions; var entityGetters = window.hass.entityGetters;
var entityActions = window.hass.entityActions;
Polymer({ Polymer({
is: 'partial-dev-set-state', is: 'partial-dev-set-state',
@ -83,7 +84,7 @@
}, },
entitySelected: function(ev) { entitySelected: function(ev) {
var state = stateStore.get(ev.detail.entityId); var state = reactor.evaluate(entityGetters.byId(ev.detail.entityId));
this.entityId = state.entityId; this.entityId = state.entityId;
this.state = state.state; this.state = state.state;
@ -99,7 +100,11 @@
return; return;
} }
stateActions.set(this.entityId, this.state, attr); entityActions.save({
entityId: this.entityId,
state: this.state,
attributes: attr,
});
}, },
computeFormClasses: function(narrow) { computeFormClasses: function(narrow) {

View File

@ -1,10 +1,13 @@
<link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html"> <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="./partial-base.html"> <link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-history-charts.html"> <link rel="import" href="../components/state-history-charts.html">
<link rel="import" href="../resources/pikaday-js.html">
<dom-module id="partial-history"> <dom-module id="partial-history">
<style> <style>
.content { .content {
@ -14,6 +17,14 @@
.content.wide { .content.wide {
padding: 8px; padding: 8px;
} }
paper-input {
max-width: 200px;
}
.narrow paper-input {
margin-left: 8px;
}
</style> </style>
<template> <template>
<partial-base narrow="[[narrow]]"> <partial-base narrow="[[narrow]]">
@ -23,6 +34,9 @@
on-click="handleRefreshClick"></paper-icon-button> on-click="handleRefreshClick"></paper-icon-button>
<div class$="[[computeContentClasses(narrow)]]"> <div class$="[[computeContentClasses(narrow)]]">
<paper-input label='Showing entries for' id='datePicker'
value='[[selectedDate]]'></paper-input>
<state-history-charts state-history="[[stateHistory]]" <state-history-charts state-history="[[stateHistory]]"
is-loading-data="[[isLoadingData]]"></state-history-charts> is-loading-data="[[isLoadingData]]"></state-history-charts>
</div> </div>
@ -31,43 +45,67 @@
</dom-module> </dom-module>
<script> <script>
(function() { (function() {
var stateHistoryActions = window.hass.stateHistoryActions; var entityHistoryGetters = window.hass.entityHistoryGetters;
var entityHistoryActions = window.hass.entityHistoryActions;
var uiActions = window.hass.uiActions;
Polymer({ Polymer({
is: 'partial-history', is: 'partial-history',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
narrow: { narrow: {
type: Boolean, type: Boolean,
}, },
isDataLoaded: {
type: Boolean,
bindNuclear: entityHistoryGetters.hasDataForCurrentDate,
observer: 'isDataLoadedChanged',
},
stateHistory: { stateHistory: {
type: Object, type: Object,
bindNuclear: entityHistoryGetters.entityHistoryForCurrentDate,
}, },
isLoadingData: { isLoadingData: {
type: Boolean, type: Boolean,
value: false, bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
},
selectedDate: {
type: String,
value: null,
bindNuclear: entityHistoryGetters.currentDate,
}, },
}, },
stateHistoryStoreChanged: function(stateHistoryStore) { isDataLoadedChanged: function(newVal) {
if (stateHistoryStore.isStale()) { if (!newVal) {
this.isLoadingData = true; entityHistoryActions.fetchSelectedDate();
stateHistoryActions.fetchAll();
} }
else {
this.isLoadingData = false;
}
this.stateHistory = stateHistoryStore.all;
}, },
handleRefreshClick: function() { handleRefreshClick: function() {
this.isLoadingData = true; entityHistoryActions.fetchSelectedDate();
stateHistoryActions.fetchAll(); },
datepickerFocus: function() {
this.datePicker.adjustPosition();
this.datePicker.gotoDate(moment('2015-06-30').toDate());
},
attached: function() {
this.datePicker = new Pikaday({
field: this.$.datePicker.inputElement,
onSelect: entityHistoryActions.changeCurrentDate,
});
},
detached: function() {
this.datePicker.destroy();
}, },
computeContentClasses: function(narrow) { computeContentClasses: function(narrow) {

View File

@ -1,17 +1,24 @@
<link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html"> <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="./partial-base.html"> <link rel="import" href="./partial-base.html">
<link rel="import" href="../components/ha-logbook.html"> <link rel="import" href="../components/ha-logbook.html">
<link rel="import" href="../components/loading-box.html">
<link rel="import" href="../resources/pikaday-js.html">
<dom-module id="partial-logbook"> <dom-module id="partial-logbook">
<style> <style>
.content { .selected-date-container {
background-color: white; padding: 0 16px;
padding: 8px; }
}
paper-input {
max-width: 200px;
}
</style> </style>
<template> <template>
<partial-base narrow="[[narrow]]"> <partial-base narrow="[[narrow]]">
@ -20,20 +27,30 @@
<paper-icon-button icon="refresh" header-buttons <paper-icon-button icon="refresh" header-buttons
on-click="handleRefresh"></paper-icon-button> on-click="handleRefresh"></paper-icon-button>
<ha-logbook entries="[[entries]]"></ha-logbook> <div>
<div class='selected-date-container'>
<paper-input label='Showing entries for' id='datePicker'
value='[[selectedDate]]' on-focus='datepickerFocus'></paper-input>
<loading-box hidden$='[[!isLoading]]'>Loading logbook entries</loading-box>
</div>
<ha-logbook entries="[[entries]]" hidden$='[[isLoading]]'></ha-logbook>
</div>
</partial-base> </partial-base>
</template> </template>
</dom-module> </dom-module>
<script> <script>
(function() { (function() {
var storeListenerMixIn = window.hass.storeListenerMixIn; var logbookGetters = window.hass.logbookGetters;
var logbookActions = window.hass.logbookActions; var logbookActions = window.hass.logbookActions;
var uiActions = window.hass.uiActions;
var dateToStr = window.hass.util.dateToStr;
Polymer({ Polymer({
is: 'partial-logbook', is: 'partial-logbook',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
narrow: { narrow: {
@ -41,22 +58,61 @@
value: false, value: false,
}, },
selectedDate: {
type: String,
bindNuclear: logbookGetters.currentDate,
},
isLoading: {
type: Boolean,
bindNuclear: logbookGetters.isLoadingEntries,
},
isStale: {
type: Boolean,
bindNuclear: logbookGetters.isCurrentStale,
observer: 'isStaleChanged',
},
entries: { entries: {
type: Array, type: Array,
value: [], bindNuclear: [
logbookGetters.currentEntries,
function(entries) { return entries.toArray(); },
],
},
datePicker: {
type: Object,
}, },
}, },
logbookStoreChanged: function(logbookStore) { isStaleChanged: function(newVal) {
if (logbookStore.isStale()) { if (newVal) {
logbookActions.fetch(); // isLoading wouldn't update without async <_<
this.async(
function() { logbookActions.fetchDate(this.selectedDate); }, 10);
} }
this.entries = logbookStore.all.toArray();
}, },
handleRefresh: function() { handleRefresh: function() {
logbookActions.fetch(); logbookActions.fetchDate(this.selectedDate);
},
datepickerFocus: function() {
this.datePicker.adjustPosition();
this.datePicker.gotoDate(moment('2015-06-30').toDate());
},
attached: function() {
this.datePicker = new Pikaday({
field: this.$.datePicker.inputElement,
onSelect: logbookActions.changeCurrentDate,
});
},
detached: function() {
this.datePicker.destroy();
}, },
}); });
})(); })();

View File

@ -1,11 +1,11 @@
<link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html"> <link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html"> <link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-cards.html"> <link rel="import" href="../components/state-cards.html">
<link rel="import" href="../components/ha-voice-command-progress.html">
<dom-module id="partial-states"> <dom-module id="partial-states">
<style> <style>
@ -41,21 +41,24 @@
<template> <template>
<partial-base narrow="[[narrow]]"> <partial-base narrow="[[narrow]]">
<span header-title>{{headerTitle}}</span> <span header-title>[[computeHeaderTitle(filter)]]</span>
<span header-buttons> <span header-buttons>
<paper-icon-button icon="refresh" class$="[[computeRefreshButtonClass(isFetching)]]" <paper-icon-button
on-click="handleRefresh" hidden$="[[isStreaming]]"></paper-icon-button> icon="refresh"
<paper-icon-button icon="[[listenButtonIcon]]" hidden$={{!canListen}} class$="[[computeRefreshButtonClass(isFetching)]]"
on-click="handleRefresh" hidden$="[[isStreaming]]"
></paper-icon-button>
<paper-icon-button
icon="[[computeListenButtonIcon(isListening)]]"
hidden$='[[!canListen]]'
on-click="handleListenClick"></paper-icon-button> on-click="handleListenClick"></paper-icon-button>
</span> </span>
<div class='content-wrapper'> <div class='content-wrapper'>
<div class='listening' hidden$="[[!showListenInterface]]" <div class='listening' hidden$="[[!showListenInterface]]"
on-click="handleListenClick"> on-click="handleListenClick">
<iron-icon icon="av:hearing"></iron-icon> <span>{{finalTranscript}}</span> <ha-voice-command-progress></ha-voice-command-progress>
<span class='interimTranscript'>[[interimTranscript]]</span>
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
</div> </div>
<state-cards states="[[states]]"> <state-cards states="[[states]]">
@ -75,28 +78,24 @@
<script> <script>
(function(){ (function(){
var configGetters = window.hass.configGetters;
var navigationGetters = window.hass.navigationGetters;
var voiceGetters = window.hass.voiceGetters;
var streamGetters = window.hass.streamGetters;
var serviceGetters = window.hass.serviceGetters;
var syncGetters = window.hass.syncGetters;
var syncActions = window.hass.syncActions; var syncActions = window.hass.syncActions;
var voiceActions = window.hass.voiceActions; var voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var uiConstants = window.hass.uiConstants; var entityDomainFilters = window.hass.util.entityDomainFilters;
Polymer({ Polymer({
is: 'partial-states', is: 'partial-states',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
/**
* Title to show in the header
*/
headerTitle: {
type: String,
value: 'States',
},
/**
* If header is to be shown in narrow mode.
*/
narrow: { narrow: {
type: Boolean, type: Boolean,
value: false, value: false,
@ -104,110 +103,56 @@
filter: { filter: {
type: String, type: String,
value: null, bindNuclear: navigationGetters.activeFilter,
observer: 'filterChanged',
},
voiceSupported: {
type: Boolean,
value: voiceActions.isSupported(),
}, },
isFetching: { isFetching: {
type: Boolean, type: Boolean,
value: false, bindNuclear: syncGetters.isFetching,
}, },
isStreaming: { isStreaming: {
type: Boolean, type: Boolean,
value: false, bindNuclear: streamGetters.isStreamingEvents,
}, },
canListen: { canListen: {
type: Boolean, type: Boolean,
value: false, bindNuclear: [
voiceGetters.isVoiceSupported,
configGetters.isComponentLoaded('conversation'),
function(isVoiceSupported, componentLoaded) {
return isVoiceSupported && componentLoaded;
}
]
}, },
isListening: { isListening: {
type: Boolean, type: Boolean,
value: false, bindNuclear: voiceGetters.isListening,
},
isTransmitting: {
type: Boolean,
value: false,
},
interimTranscript: {
type: String,
value: '',
},
finalTranscript: {
type: String,
value: '',
},
listenButtonIcon: {
type: String,
computed: 'computeListenButtonIcon(isListening)'
}, },
showListenInterface: { showListenInterface: {
type: Boolean, type: Boolean,
computed: 'computeShowListenInterface(isListening,isTransmitting)' bindNuclear: [
} voiceGetters.isListening,
}, voiceGetters.isTransmitting,
function(isListening, isTransmitting) {
return isListening || isTransmitting;
},
],
},
componentStoreChanged: function(componentStore) { states: {
this.canListen = this.voiceSupported && type: Array,
componentStore.isLoaded('conversation'); bindNuclear: [
}, navigationGetters.filteredStates,
// are here so a change to services causes a re-render.
stateStoreChanged: function() { // we need this to decide if we show toggles for states.
this.refreshStates(); serviceGetters.entityMap,
}, function(states) { return states.toArray(); },
],
syncStoreChanged: function(syncStore) { },
this.isFetching = syncStore.isFetching;
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
voiceStoreChanged: function(voiceStore) {
this.isListening = voiceStore.isListening;
this.isTransmitting = voiceStore.isTransmitting;
this.finalTranscript = voiceStore.finalTranscript;
this.interimTranscript = voiceStore.interimTranscript.slice(
this.finalTranscript.length);
},
filterChanged: function() {
this.refreshStates();
this.headerTitle = uiConstants.STATE_FILTERS[this.filter] || 'States';
},
refreshStates: function() {
var states;
if (this.filter) {
var filter = this.filter;
states = stateStore.all.filter(function(state) {
return state.domain === filter;
});
} else {
// all but the STATE_FILTER keys
states = stateStore.all.filter(function(state) {
return !(state.domain in uiConstants.STATE_FILTERS);
});
}
this.states = states.toArray().filter(
function (el) {return !el.attributes.hidden;});
}, },
handleRefresh: function() { handleRefresh: function() {
@ -222,12 +167,12 @@
} }
}, },
computeListenButtonIcon: function(isListening) { computeHeaderTitle: function(filter) {
return isListening ? 'av:mic-off' : 'av:mic'; return filter ? entityDomainFilters[filter] : 'States';
}, },
computeShowListenInterface: function(isListening,isTransmitting) { computeListenButtonIcon: function(isListening) {
return isListening || isTransmitting; return isListening ? 'av:mic-off' : 'av:mic';
}, },
computeRefreshButtonClass: function(isFetching) { computeRefreshButtonClass: function(isFetching) {

View File

@ -1,30 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../dialogs/more-info-dialog.html">
<dom-module id="modal-manager">
<template>
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
</template>
</dom-module>
<script>
(function() {
var uiConstants = window.hass.uiConstants,
dispatcher = window.hass.dispatcher;
Polymer({
is: 'modal-manager',
ready: function() {
dispatcher.register(function(payload) {
switch (payload.actionType) {
case uiConstants.ACTION_SHOW_DIALOG_MORE_INFO:
this.$.moreInfoDialog.show(payload.entityId);
break;
}
}.bind(this));
},
});
})();
</script>

View File

@ -15,33 +15,26 @@
<script> <script>
(function() { (function() {
var notificationGetters = window.hass.notificationGetters;
Polymer({ Polymer({
is: 'notification-manager', is: 'notification-manager',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
text: { text: {
type: String, type: String,
value: '', bindNuclear: notificationGetters.lastNotificationMessage,
}, observer: 'showNotification',
lastId: {
type: Number,
}, },
}, },
notificationStoreChanged: function(notificationStore) { showNotification: function(newText) {
if (notificationStore.hasNewNotifications(this.lastId)) { if (newText) {
var notification = notificationStore.lastNotification;
this.lastId = notification.id;
this.text = notification.message;
this.$.toast.show(); this.$.toast.show();
} }
}, }
}); });
})(); })();
</script> </script>

View File

@ -0,0 +1,40 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<script>
(function() {
var authGetters = window.hass.authGetters;
var streamGetters = window.hass.streamGetters;
Polymer({
is: 'preferences-manager',
behaviors: [nuclearObserver],
properties: {
authToken: {
type: String,
bindNuclear: authGetters.currentAuthToken,
observer: 'updateStorage',
},
useStreaming: {
type: String,
bindNuclear: ,
observer: 'updateStorage',
},
},
updateStorage: function() {
if (!('localStorage' in window)) {
return;
}
var storage = localStorage;
Object.keys(this.properties).forEach(function(prop) {
storage[prop] = this.prop;
});
},
});
})();
</script>

View File

@ -52,13 +52,14 @@
<script> <script>
(function() { (function() {
var streamGetters = window.hass.streamGetters;
var syncActions = window.hass.syncActions; var syncActions = window.hass.syncActions;
var serviceActions = window.hass.serviceActions; var serviceActions = window.hass.serviceActions;
Polymer({ Polymer({
is: 'more-info-configurator', is: 'more-info-configurator',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
stateObj: { stateObj: {
@ -72,7 +73,7 @@
isStreaming: { isStreaming: {
type: Boolean, type: Boolean,
value: false, bindNuclear: streamGetters.isStreamingEvents,
}, },
isConfigurable: { isConfigurable: {
@ -99,10 +100,6 @@
return stateObj.attributes.submit_caption || 'Set configuration'; return stateObj.attributes.submit_caption || 'Set configuration';
}, },
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
submitClicked: function() { submitClicked: function() {
this.isConfiguring = true; this.isConfiguring = true;

View File

@ -23,28 +23,36 @@
<script> <script>
(function() { (function() {
var stateStore = window.hass.stateStore; var entityGetters = window.hass.entityGetters;
var moreInfoGetters = window.hass.moreInfoGetters;
Polymer({ Polymer({
is: 'more-info-group', is: 'more-info-group',
behaviors: [StoreListenerBehavior], behaviors: [nuclearObserver],
properties: { properties: {
stateObj: { stateObj: {
type: Object, type: Object,
observer: 'updateStates',
}, },
states: { states: {
type: Array, type: Array,
value: [], bindNuclear: [
moreInfoGetters.currentEntity,
entityGetters.entityMap,
function(currentEntity, entities) {
// weird bug??
if (!currentEntity) {
return;
}
return currentEntity.attributes.entity_id.map(
entities.get.bind(entities));
},
],
}, },
}, },
stateStoreChanged: function() {
this.updateStates();
},
updateStates: function() { updateStates: function() {
this.states = this.stateObj && this.stateObj.attributes.entity_id ? this.states = this.stateObj && this.stateObj.attributes.entity_id ?

View File

@ -1,7 +1,7 @@
<link rel='import' href='../bower_components/polymer/polymer.html'> <link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/paper-slider/paper-slider.html'> <link rel='import' href='../bower_components/paper-slider/paper-slider.html'>
<link rel='import' href='../bower_components/color-picker-element/dist/color-picker.html'> <link rel='import' href='../components/ha-color-picker.html'>
<dom-module id='more-info-light'> <dom-module id='more-info-light'>
<style> <style>
@ -13,7 +13,7 @@
transition: max-height .5s ease-in; transition: max-height .5s ease-in;
} }
color-picker { ha-color-picker {
display: block; display: block;
width: 350px; width: 350px;
margin: 0 auto; margin: 0 auto;
@ -27,7 +27,7 @@
max-height: 40px; max-height: 40px;
} }
.has-xy_color color-picker { .has-xy_color ha-color-picker {
max-height: 500px; max-height: 500px;
} }
</style> </style>
@ -41,7 +41,7 @@
</paper-slider> </paper-slider>
</div> </div>
<color-picker on-colorselected='colorPicked' width='350' height='200'> <ha-color-picker on-colorselected='colorPicked' width='350' height='200'>
</color-picker> </color-picker>
</div> </div>
</template> </template>
@ -73,7 +73,7 @@
this.brightnessSliderValue = newVal.attributes.brightness; this.brightnessSliderValue = newVal.attributes.brightness;
} }
this.debounce('more-info-light-animation-finish', function() { this.async(function() {
this.fire('iron-resize'); this.fire('iron-resize');
}.bind(this), 500); }.bind(this), 500);
}, },

View File

@ -8,8 +8,7 @@
text-transform: capitalize; text-transform: capitalize;
} }
/* Accent the power button because the user should use that first */ paper-icon-button[highlight] {
paper-icon-button[focus] {
color: var(--accent-color); color: var(--accent-color);
} }
@ -21,7 +20,7 @@
transition: max-height .5s ease-in; transition: max-height .5s ease-in;
} }
.has-media_volume .volume { .has-volume_level .volume {
max-height: 40px; max-height: 40px;
} }
</style> </style>
@ -29,25 +28,26 @@
<div class$='[[computeClassNames(stateObj)]]'> <div class$='[[computeClassNames(stateObj)]]'>
<div class='layout horizontal'> <div class='layout horizontal'>
<div class='flex'> <div class='flex'>
<paper-icon-button icon='power-settings-new' focus$='[[isIdle]]' <paper-icon-button icon='power-settings-new' highlight$='[[isOff]]'
on-tap='handleTogglePower'></paper-icon-button> on-tap='handleTogglePower'
hidden$='[[computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff)]]'></paper-icon-button>
</div> </div>
<div> <div>
<template is='dom-if' if='[[!isIdle]]'> <template is='dom-if' if='[[!isOff]]'>
<paper-icon-button icon='av:skip-previous' <paper-icon-button icon='av:skip-previous' on-tap='handlePrevious'
on-tap='handlePrevious'></paper-icon-button> hidden$='[[!supportsPreviousTrack]]'></paper-icon-button>
<paper-icon-button icon='[[computePlayPauseIcon(stateObj)]]' focus$ <paper-icon-button icon='[[computePlaybackControlIcon(stateObj)]]'
on-tap='handlePlayPause'></paper-icon-button> on-tap='handlePlaybackControl' highlight></paper-icon-button>
<paper-icon-button icon='av:skip-next' <paper-icon-button icon='av:skip-next' on-tap='handleNext'
on-tap='handleNext'></paper-icon-button> hidden$='[[!supportsNextTrack]]'></paper-icon-button>
</template> </template>
</div> </div>
</div> </div>
<div class='volume center horizontal layout'> <div class='volume center horizontal layout' hidden$='[[!supportsVolumeSet]]'>
<paper-icon-button on-tap="handleVolumeTap" <paper-icon-button on-tap="handleVolumeTap"
icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button> icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button>
<paper-slider hidden='[[isMuted]]' <paper-slider disabled$='[[isMuted]]'
min='0' max='100' value='{{volumeSliderValue}}' min='0' max='100' value='[[volumeSliderValue]]'
on-change='volumeSliderChanged' class='flex'> on-change='volumeSliderChanged' class='flex'>
</paper-slider> </paper-slider>
</div> </div>
@ -59,7 +59,7 @@
(function() { (function() {
var serviceActions = window.hass.serviceActions; var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil; var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['media_volume']; var ATTRIBUTE_CLASSES = ['volume_level'];
Polymer({ Polymer({
is: 'more-info-media_player', is: 'more-info-media_player',
@ -70,9 +70,14 @@
observer: 'stateObjChanged', observer: 'stateObjChanged',
}, },
isIdle: { isOff: {
type: Boolean, type: Boolean,
computed: 'computeIsIdle(stateObj)', value: false,
},
isPlaying: {
type: Boolean,
value: false,
}, },
isMuted: { isMuted: {
@ -83,53 +88,98 @@
volumeSliderValue: { volumeSliderValue: {
type: Number, type: Number,
value: 0, 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) { if (newVal) {
this.volumeSliderValue = newVal.attributes.media_volume * 100; this.isOff = newVal.state == 'off';
this.isMuted = newVal.attributes.media_is_volume_muted; 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.async(function() { this.fire('iron-resize'); }.bind(this), 500);
this.fire('iron-resize');
}.bind(this), 500);
}, },
computeClassNames: function(stateObj) { computeClassNames: function(stateObj) {
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES); return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
}, },
computeMediaState: function(stateObj) { computeIsOff: function(stateObj) {
return stateObj.state == 'idle' ? 'idle' : stateObj.attributes.media_state; return stateObj.state == 'off';
},
computeIsIdle: function(stateObj) {
return stateObj.state == 'idle';
},
computePowerButtonCaption: function(isIdle) {
return isIdle ? 'Turn on' : 'Turn off';
}, },
computeMuteVolumeIcon: function(isMuted) { computeMuteVolumeIcon: function(isMuted) {
return isMuted ? 'av:volume-off' : 'av:volume-up'; return isMuted ? 'av:volume-off' : 'av:volume-up';
}, },
computePlayPauseIcon: function(stateObj) { computePlaybackControlIcon: function(stateObj) {
return stateObj.attributes.media_state == 'playing' ? 'av:pause' : 'av:play-arrow'; 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() { handleTogglePower: function() {
this.callService(this.isIdle ? 'turn_on' : 'turn_off'); this.callService(this.isOff ? 'turn_on' : 'turn_off');
}, },
handlePrevious: function() { 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'); this.callService('media_play_pause');
}, },
@ -138,14 +188,16 @@
}, },
handleVolumeTap: function() { 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) { volumeSliderChanged: function(ev) {
var volPercentage = parseFloat(ev.target.value); var volPercentage = parseFloat(ev.target.value);
var vol = volPercentage > 0 ? volPercentage / 100 : 0; var vol = volPercentage > 0 ? volPercentage / 100 : 0;
this.callService('volume_set', { volume_level: vol });
this.callService('volume_set', { volume: vol });
}, },
callService: function(service, data) { callService: function(service, data) {

View File

@ -41,7 +41,7 @@
<script> <script>
(function() { (function() {
var constants = window.hass.constants; var temperatureUnits = window.hass.util.temperatureUnits;
var serviceActions = window.hass.serviceActions; var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil; var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['away_mode']; var ATTRIBUTE_CLASSES = ['away_mode'];
@ -76,7 +76,8 @@
this.targetTemperatureSliderValue = this.stateObj.state; this.targetTemperatureSliderValue = this.stateObj.state;
this.awayToggleChecked = this.stateObj.attributes.away_mode == 'on'; this.awayToggleChecked = this.stateObj.attributes.away_mode == 'on';
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) { if (this.stateObj.attributes.unit_of_measurement ===
temperatureUnits.UNIT_TEMP_F) {
this.tempMin = 45; this.tempMin = 45;
this.tempMax = 95; this.tempMax = 95;
} else { } else {

View File

@ -48,7 +48,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "media_player": case "media_player":
var icon = "hardware:cast"; var icon = "hardware:cast";
if (state && state !== "idle") { if (state && state !== "off" && state !== 'idle') {
icon += "-connected"; icon += "-connected";
} }

View File

@ -10,52 +10,16 @@
'sensor', 'sensor',
]; ];
// Add some frontend specific helpers to the models var reactor = window.hass.reactor;
Object.defineProperties(window.hass.stateModel.prototype, { var serviceGetters = window.hass.serviceGetters;
// how to render the card for this state var authActions = window.hass.authActions;
cardType: { var preferences = window.hass.localStoragePreferences;
get: function() {
console.warn('Deprecated method. Please use hass.uiUtil.stateCardType');
return window.hass.uiUtil.stateCardType(this);
}
},
// how to render the more info of this state
moreInfoType: {
get: function() {
console.warn('Deprecated method. Please use hass.uiUtil.stateMoreInfoType');
return window.hass.uiUtil.stateMoreInfoType(this);
}
},
});
var dispatcher = window.hass.dispatcher,
constants = window.hass.constants,
preferenceStore = window.hass.preferenceStore,
authActions = window.hass.authActions;
window.hass.uiConstants = {
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
STATE_FILTERS: {
'group': 'Groups',
'script': 'Scripts',
'scene': 'Scenes',
},
};
window.hass.uiActions = { window.hass.uiActions = {
showMoreInfoDialog: function(entityId) { validateAuth: function(authToken, rememberAuth) {
dispatcher.dispatch({
actionType: window.hass.uiConstants.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
validateAuth: function(authToken, rememberLogin) {
authActions.validate(authToken, { authActions.validate(authToken, {
useStreaming: preferenceStore.useStreaming, rememberAuth: rememberAuth,
rememberLogin: rememberLogin, useStreaming: preferences.useStreaming,
}); });
}, },
}; };
@ -65,7 +29,7 @@
stateCardType: function(state) { stateCardType: function(state) {
if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) { if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) {
return state.domain; return state.domain;
} else if(state.canToggle) { } else if(reactor.evaluate(serviceGetters.canToggle(state.entityId))) {
return "toggle"; return "toggle";
} else { } else {
return "display"; return "display";

View File

@ -0,0 +1,5 @@
<!--
Wrapping JS in an HTML file will prevent it from being loaded twice.
-->
<script src="../bower_components/lodash/lodash.min.js"></script>

View File

@ -2,7 +2,7 @@
Wrapping JS in an HTML file will prevent it from being loaded twice. Wrapping JS in an HTML file will prevent it from being loaded twice.
--> -->
<script src="../bower_components/moment/moment.js"></script> <script src="../bower_components/moment/min/moment.min.js"></script>
<script> <script>
window.hass.uiUtil.formatTime = function(dateObj) { window.hass.uiUtil.formatTime = function(dateObj) {

View File

@ -0,0 +1,2 @@
<script src="../bower_components/pikaday/pikaday.js"></script>
<link href="../bower_components/pikaday/css/pikaday.css" media="all" rel="stylesheet" />

View File

@ -1,21 +1,42 @@
<script> <script>
(function() { (function() {
var NuclearObserver = function NuclearObserver(reactor) {
return {
var StoreListenerMixIn = window.hass.storeListenerMixIn; attached: function() {
var component = this;
this.__unwatchFns = Object.keys(component.properties).reduce(
function(unwatchFns, key) {
if (!('bindNuclear' in component.properties[key])) {
return unwatchFns;
}
var getter = component.properties[key].bindNuclear;
window.StoreListenerBehavior = { if (!getter) {
throw 'Undefined getter specified for key ' + key;
}
attached: function() { // console.log(key, getter);
StoreListenerMixIn.listenToStores(true, this);
},
detached: function() { component[key] = reactor.evaluate(getter);
StoreListenerMixIn.stopListeningToStores(this);
},
return unwatchFns.concat(reactor.observe(getter, function(val) {
// console.log('New value for', key, val);
component[key] = val;
}));
}, []);
},
detached: function() {
while (this.__unwatchFns.length) {
this.__unwatchFns.shift()();
}
},
};
}; };
window.nuclearObserver = NuclearObserver(window.hass.reactor);
})(); })();
</script> </script>

File diff suppressed because one or more lines are too long

View File

@ -9,12 +9,16 @@ from datetime import timedelta
from itertools import groupby from itertools import groupby
from collections import defaultdict from collections import defaultdict
import homeassistant.util.dt as date_util import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder import homeassistant.components.recorder as recorder
from homeassistant.const import HTTP_BAD_REQUEST
DOMAIN = 'history' DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'http']
URL_HISTORY_PERIOD = re.compile(
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
def last_5_states(entity_id): def last_5_states(entity_id):
""" Return the last 5 states for entity_id. """ """ Return the last 5 states for entity_id. """
@ -111,8 +115,7 @@ def setup(hass, config):
r'recent_states'), r'recent_states'),
_api_last_5_states) _api_last_5_states)
hass.http.register_path( hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
'GET', re.compile(r'/api/history/period'), _api_history_period)
return True return True
@ -128,10 +131,25 @@ def _api_last_5_states(handler, path_match, data):
def _api_history_period(handler, path_match, data): def _api_history_period(handler, path_match, data):
""" Return history over a period of time. """ """ Return history over a period of time. """
# 1 day for now.. date_str = path_match.group('date')
start_time = date_util.utcnow() - timedelta(seconds=86400) 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') entity_id = data.get('filter_entity_id')
handler.write_json( 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())

View File

@ -28,6 +28,7 @@ DISCOVER_SENSORS = "isy994.sensors"
ISY = None ISY = None
SENSOR_STRING = 'Sensor' SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}' HIDDEN_STRING = '{HIDE ME}'
CONF_TLS_VER = 'tls'
# setup logger # setup logger
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,7 +43,6 @@ def setup(hass, config):
import PyISY import PyISY
except ImportError: except ImportError:
_LOGGER.error("Error while importing dependency PyISY.") _LOGGER.error("Error while importing dependency PyISY.")
return False return False
# pylint: disable=global-statement # pylint: disable=global-statement
@ -74,10 +74,12 @@ def setup(hass, config):
global HIDDEN_STRING global HIDDEN_STRING
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING)) SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING))
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING)) HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
tls_version = config[DOMAIN].get(CONF_TLS_VER, None)
# connect to ISY controller # connect to ISY controller
global ISY 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: if not ISY.connected:
return False return False

View File

@ -8,7 +8,7 @@ import logging
from homeassistant.const import ( from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, 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) SERVICE_MEDIA_PLAY_PAUSE)
@ -43,7 +43,7 @@ def media_next_track(hass):
def media_prev_track(hass): def media_prev_track(hass):
""" Press the keyboard button for prev track. """ """ 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): def setup(hass, config):
@ -79,7 +79,7 @@ def setup(hass, config):
lambda service: lambda service:
keyboard.tap_key(keyboard.media_next_track_key)) 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: lambda service:
keyboard.tap_key(keyboard.media_prev_track_key)) keyboard.tap_key(keyboard.media_prev_track_key))

View File

@ -53,6 +53,7 @@ import os
import csv import csv
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util import homeassistant.util as util
from homeassistant.const import ( from homeassistant.const import (
@ -87,6 +88,10 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short" FLASH_SHORT = "short"
FLASH_LONG = "long" 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" LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms # Maps discovered services to their platforms
@ -96,6 +101,11 @@ DISCOVERY_PLATFORMS = {
discovery.services.PHILIPS_HUE: 'hue', discovery.services.PHILIPS_HUE: 'hue',
} }
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_xy': ATTR_XY_COLOR,
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -108,7 +118,8 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None, 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. """ """ Turns all or specified light on. """
data = { data = {
key: value for key, value in [ 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_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color), (ATTR_XY_COLOR, xy_color),
(ATTR_FLASH, flash), (ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
] if value is not None ] if value is not None
} }
@ -247,11 +259,16 @@ def setup(hass, config):
elif dat[ATTR_FLASH] == FLASH_LONG: elif dat[ATTR_FLASH] == FLASH_LONG:
params[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: for light in target_lights:
light.turn_on(**params) light.turn_on(**params)
for light in target_lights: 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 # Listen for light on and light off service calls
hass.services.register(DOMAIN, SERVICE_TURN_ON, hass.services.register(DOMAIN, SERVICE_TURN_ON,
@ -261,3 +278,41 @@ def setup(hass, config):
handle_light_service) handle_light_service)
return True 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

View File

@ -7,9 +7,8 @@ Demo platform that implements lights.
""" """
import random import random
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.light import (
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR)
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR
LIGHT_COLORS = [ LIGHT_COLORS = [
@ -22,16 +21,16 @@ LIGHT_COLORS = [
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo lights. """ """ Find and return demo lights. """
add_devices_callback([ add_devices_callback([
DemoLight("Bed Light", STATE_OFF), DemoLight("Bed Light", False),
DemoLight("Ceiling", STATE_ON), DemoLight("Ceiling", True),
DemoLight("Kitchen", STATE_ON) DemoLight("Kitchen", True)
]) ])
class DemoLight(ToggleEntity): class DemoLight(Light):
""" Provides a demo switch. """ """ Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180): def __init__(self, name, state, xy=None, brightness=180):
self._name = name or DEVICE_DEFAULT_NAME self._name = name
self._state = state self._state = state
self._xy = xy or random.choice(LIGHT_COLORS) self._xy = xy or random.choice(LIGHT_COLORS)
self._brightness = brightness self._brightness = brightness
@ -47,27 +46,23 @@ class DemoLight(ToggleEntity):
return self._name return self._name
@property @property
def state(self): def brightness(self):
""" Returns the name of the device if any. """ """ Brightness of this light between 0..255. """
return self._state return self._brightness
@property @property
def state_attributes(self): def color_xy(self):
""" Returns optional state attributes. """ """ XY color value. """
if self.is_on: return self._xy
return {
ATTR_BRIGHTNESS: self._brightness,
ATTR_XY_COLOR: self._xy,
}
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
return self._state == STATE_ON return self._state
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
self._state = STATE_ON self._state = True
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
self._xy = kwargs[ATTR_XY_COLOR] self._xy = kwargs[ATTR_XY_COLOR]
@ -75,6 +70,9 @@ class DemoLight(ToggleEntity):
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turn the device off. """ """ Turn the device off. """
self._state = STATE_OFF self._state = False
self.update_ha_state()

View File

@ -6,11 +6,11 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.const import CONF_HOST
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT) ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT,
EFFECT_COLORLOOP)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) 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 """ """ Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights): def __init__(self, light_id, info, bridge, update_lights):
@ -149,19 +149,17 @@ class HueLight(ToggleEntity):
@property @property
def name(self): def name(self):
""" Get the mame of the Hue light. """ """ Get the mame of the Hue light. """
return self.info.get('name', 'No name') return self.info.get('name', DEVICE_DEFAULT_NAME)
@property @property
def state_attributes(self): def brightness(self):
""" Returns optional state attributes. """ """ Brightness of this light between 0..255. """
attr = {} return self.info['state']['bri']
if self.is_on: @property
attr[ATTR_BRIGHTNESS] = self.info['state']['bri'] def color_xy(self):
if 'xy' in self.info['state']: """ XY color value. """
attr[ATTR_XY_COLOR] = self.info['state']['xy'] return self.info['state'].get('xy')
return attr
@property @property
def is_on(self): def is_on(self):
@ -194,6 +192,13 @@ class HueLight(ToggleEntity):
else: else:
command['alert'] = 'none' 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) self.bridge.set_light(self.light_id, command)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):

View File

@ -23,9 +23,8 @@ light:
""" """
import logging import logging
from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.components.light import ATTR_BRIGHTNESS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,18 +42,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
lights = [] lights = []
for i in range(1, 5): for i in range(1, 5):
if 'group_%d_name' % (i) in config: if 'group_%d_name' % (i) in config:
lights.append( lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
LimitlessLED(
led,
i,
config['group_%d_name' % (i)]
)
)
add_devices_callback(lights) add_devices_callback(lights)
class LimitlessLED(ToggleEntity): class LimitlessLED(Light):
""" Represents a LimitlessLED light """ """ Represents a LimitlessLED light """
def __init__(self, led, group, name): def __init__(self, led, group, name):
@ -65,7 +58,7 @@ class LimitlessLED(ToggleEntity):
self.led.off(self.group) self.led.off(self.group)
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME
self._state = STATE_OFF self._state = False
self._brightness = 100 self._brightness = 100
@property @property
@ -79,33 +72,26 @@ class LimitlessLED(ToggleEntity):
return self._name return self._name
@property @property
def state(self): def brightness(self):
""" Returns the name of the device if any. """ return self._brightness
return self._state
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.is_on:
return {
ATTR_BRIGHTNESS: self._brightness,
}
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
return self._state == STATE_ON return self._state
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
self._state = STATE_ON self._state = True
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
self.led.set_brightness(self._brightness, self.group) self.led.set_brightness(self._brightness, self.group)
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turn the device off. """ """ Turn the device off. """
self._state = STATE_OFF self._state = False
self.led.off(self.group) self.led.off(self.group)
self.update_ha_state()

View File

@ -1,9 +1,8 @@
""" Support for Tellstick lights. """ """ Support for Tellstick lights. """
import logging import logging
# pylint: disable=no-name-in-module, import-error # 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.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants 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) add_devices_callback(lights)
class TellstickLight(ToggleEntity): class TellstickLight(Light):
""" Represents a tellstick light """ """ Represents a tellstick light """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF | tellcore_constants.TELLSTICK_TURNOFF |
@ -38,7 +37,7 @@ class TellstickLight(ToggleEntity):
def __init__(self, tellstick): def __init__(self, tellstick):
self.tellstick = tellstick self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self.brightness = 0 self._brightness = 0
@property @property
def name(self): def name(self):
@ -48,34 +47,28 @@ class TellstickLight(ToggleEntity):
@property @property
def is_on(self): def is_on(self):
""" True if switch is on. """ """ 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): def turn_off(self, **kwargs):
""" Turns the switch off. """ """ Turns the switch off. """
self.tellstick.turn_off() self.tellstick.turn_off()
self.brightness = 0 self._brightness = 0
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is None: if brightness is None:
self.brightness = 255 self._brightness = 255
else: else:
self.brightness = brightness self._brightness = brightness
self.tellstick.dim(self.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
def update(self): def update(self):
""" Update state of the light. """ """ Update state of the light. """
@ -83,12 +76,12 @@ class TellstickLight(ToggleEntity):
self.last_sent_command_mask) self.last_sent_command_mask)
if last_command == tellcore_constants.TELLSTICK_TURNON: if last_command == tellcore_constants.TELLSTICK_TURNON:
self.brightness = 255 self._brightness = 255
elif last_command == tellcore_constants.TELLSTICK_TURNOFF: elif last_command == tellcore_constants.TELLSTICK_TURNOFF:
self.brightness = 0 self._brightness = 0
elif (last_command == tellcore_constants.TELLSTICK_DIM or elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or last_command == tellcore_constants.TELLSTICK_UP or
last_command == tellcore_constants.TELLSTICK_DOWN): last_command == tellcore_constants.TELLSTICK_DOWN):
last_sent_value = self.tellstick.last_sent_value() last_sent_value = self.tellstick.last_sent_value()
if last_sent_value is not None: if last_sent_value is not None:
self.brightness = last_sent_value self._brightness = last_sent_value

View File

@ -4,12 +4,14 @@ homeassistant.components.logbook
Parses events and generates a human log. Parses events and generates a human log.
""" """
from datetime import timedelta
from itertools import groupby from itertools import groupby
import re
from homeassistant import State, DOMAIN as HA_DOMAIN from homeassistant import State, DOMAIN as HA_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF, 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.util.dt as dt_util
import homeassistant.components.recorder as recorder import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun import homeassistant.components.sun as sun
@ -17,12 +19,10 @@ import homeassistant.components.sun as sun
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = '/api/logbook' URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """ QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ? SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
ORDER BY time_fired
""" """
GROUP_BY_MINUTES = 15 GROUP_BY_MINUTES = 15
@ -37,11 +37,26 @@ def setup(hass, config):
def _handle_get_logbook(handler, path_match, data): def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """ """ 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( if date_str:
recorder.query_events( start_date = dt_util.date_str_to_date(date_str)
QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),))))
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): class Entry(object):

View File

@ -10,11 +10,12 @@ from homeassistant.components import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import ( 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_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, 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' DOMAIN = 'media_player'
DEPENDENCIES = [] DEPENDENCIES = []
@ -28,28 +29,70 @@ DISCOVERY_PLATFORMS = {
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
STATE_NO_APP = 'idle' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
ATTR_STATE = 'state' ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_OPTIONS = 'options'
ATTR_MEDIA_STATE = 'media_state'
ATTR_MEDIA_CONTENT_ID = 'media_content_id' 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_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist' ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM = 'media_album' ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
ATTR_MEDIA_IMAGE_URL = 'media_image_url' ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
ATTR_MEDIA_VOLUME = 'media_volume' ATTR_MEDIA_TRACK = 'media_track'
ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted' ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
ATTR_MEDIA_DURATION = 'media_duration' ATTR_MEDIA_SEASON = 'media_season'
ATTR_MEDIA_DATE = 'media_date' 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_TYPE_MUSIC = 'music'
MEDIA_STATE_PLAYING = 'playing' MEDIA_TYPE_TVSHOW = 'tvshow'
MEDIA_STATE_PAUSED = 'paused' MEDIA_TYPE_VIDEO = 'movie'
MEDIA_STATE_STOPPED = 'stopped'
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): 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) 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) 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) hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
def volume_mute(hass, entity_id=None): def mute_volume(hass, mute, entity_id=None):
""" Send the media player the command to toggle its mute state. """ """ Send the media player the command for volume down. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data = {ATTR_MEDIA_VOLUME_MUTED: mute}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data)
def volume_set(hass, entity_id=None, volume=None): def set_volume_level(hass, volume, entity_id=None):
""" Set volume on media player. """ """ Send the media player the command for volume down. """
data = { data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id), if entity_id:
(ATTR_MEDIA_VOLUME, volume), data[ATTR_ENTITY_ID] = entity_id
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) 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) 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. """ """ Send the media player the command for prev track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data) hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_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',
}
def setup(hass, config): def setup(hass, config):
@ -180,35 +211,56 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler) 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. """ """ Set specified volume on the media player. """
target_players = component.extract_from_service(service) 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: for player in target_players:
player.volume_set(volume) player.set_volume_level(volume)
if player.should_poll: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
lambda service:
volume_set_service(
service, service.data.get('volume')))
def volume_mute_service(service, mute): def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """ """ Mute (true) or unmute (false) the media player. """
target_players = component.extract_from_service(service) 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: for player in target_players:
player.volume_mute(mute) player.mute_volume(mute)
if player.should_poll: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
lambda service:
volume_mute_service( def media_seek_service(service):
service, service.data.get('mute'))) """ 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): def play_youtube_video_service(service, media_id):
""" Plays specified media_id on the media player. """ """ Plays specified media_id on the media player. """
@ -239,51 +291,217 @@ def setup(hass, config):
class MediaPlayerDevice(Entity): class MediaPlayerDevice(Entity):
""" ABC for media player devices. """ """ 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): def turn_on(self):
""" turn media player on. """ """ turn the media player on. """
pass raise NotImplementedError()
def turn_off(self): def turn_off(self):
""" turn media player off. """ """ turn the media player off. """
pass raise NotImplementedError()
def volume_up(self): def mute_volume(self, mute):
""" volume_up media player. """ """ mute the volume. """
pass raise NotImplementedError()
def volume_down(self): def set_volume_level(self, volume):
""" volume_down media player. """ """ set volume level, range 0..1. """
pass raise NotImplementedError()
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 media_play(self): def media_play(self):
""" media_play media player. """ """ Send play commmand. """
pass raise NotImplementedError()
def media_pause(self): def media_pause(self):
""" media_pause media player. """ """ Send pause command. """
pass raise NotImplementedError()
def media_prev_track(self): def media_previous_track(self):
""" media_prev_track media player. """ """ Send previous track command. """
pass raise NotImplementedError()
def media_next_track(self): def media_next_track(self):
""" media_next_track media player. """ """ Send next track command. """
pass raise NotImplementedError()
def media_seek(self, position):
""" Send seek command. """
raise NotImplementedError()
def play_youtube(self, media_id): def play_youtube(self, media_id):
""" Plays a YouTube media. """ """ 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

View File

@ -14,18 +14,21 @@ try:
except ImportError: except ImportError:
pychromecast = None 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 ( from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, ATTR_MEDIA_TITLE, MediaPlayerDevice,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_DURATION, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
ATTR_MEDIA_VOLUME, ATTR_MEDIA_IS_VOLUME_MUTED, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
MEDIA_STATE_PLAYING, MEDIA_STATE_PAUSED, MEDIA_STATE_STOPPED, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_STATE_UNKNOWN) MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' 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 # pylint: disable=unused-argument
@ -61,6 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CastDevice(MediaPlayerDevice): class CastDevice(MediaPlayerDevice):
""" Represents a Cast device on the network. """ """ Represents a Cast device on the network. """
# pylint: disable=too-many-public-methods
def __init__(self, host): def __init__(self, host):
self.cast = pychromecast.Chromecast(host) self.cast = pychromecast.Chromecast(host)
self.youtube = youtube.YouTubeController() self.youtube = youtube.YouTubeController()
@ -73,6 +78,8 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status self.media_status = self.cast.media_controller.status
# Entity properties and methods
@property @property
def should_poll(self): def should_poll(self):
return False return False
@ -82,57 +89,121 @@ class CastDevice(MediaPlayerDevice):
""" Returns the name of the device. """ """ Returns the name of the device. """
return self.cast.device.friendly_name return self.cast.device.friendly_name
# MediaPlayerDevice properties and methods
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """ State of the player. """
if self.cast.is_idle: if self.media_status is None:
return STATE_NO_APP 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: else:
return self.cast.app_display_name return STATE_UNKNOWN
@property @property
def media_state(self): def volume_level(self):
""" Returns the media state. """ """ Volume level of the media player (0..1). """
media_controller = self.cast.media_controller return self.cast_status.volume_level if self.cast_status else None
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
@property @property
def state_attributes(self): def is_volume_muted(self):
""" Returns the state attributes. """ """ Boolean if volume is currently muted. """
cast_status = self.cast_status return self.cast_status.volume_muted if self.cast_status else None
media_status = self.media_status
media_controller = self.cast.media_controller
state_attr = { @property
ATTR_MEDIA_STATE: self.media_state, def media_content_id(self):
'application_id': self.cast.app_id, """ Content ID of current playing media. """
} return self.media_status.content_id if self.media_status else None
if cast_status: @property
state_attr[ATTR_MEDIA_VOLUME] = cast_status.volume_level def media_content_type(self):
state_attr[ATTR_MEDIA_IS_VOLUME_MUTED] = cast_status.volume_muted """ 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: @property
state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id 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: @property
state_attr[ATTR_MEDIA_DURATION] = media_status.duration def media_image_url(self):
""" Image url of current playing media. """
if self.media_status is None:
return None
if media_controller.title: images = self.media_status.images
state_attr[ATTR_MEDIA_TITLE] = media_controller.title
if media_controller.thumbnail: return images[0].url if images else None
state_attr[ATTR_ENTITY_PICTURE] = media_controller.thumbnail
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): def turn_on(self):
""" Turns on the ChromeCast. """ """ Turns on the ChromeCast. """
@ -145,57 +216,42 @@ class CastDevice(MediaPlayerDevice):
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
def turn_off(self): def turn_off(self):
""" Service to exit any running app on the specimedia player ChromeCast and """ Turns Chromecast off. """
shows idle screen. Will quit all ChromeCasts if nothing specified.
"""
self.cast.quit_app() self.cast.quit_app()
def volume_up(self): def mute_volume(self, mute):
""" Service to send the chromecast the command for volume up. """ """ mute the volume. """
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. """
self.cast.set_volume_muted(mute) self.cast.set_volume_muted(mute)
def volume_set(self, volume): def set_volume_level(self, volume):
""" Set media player volume, range of volume 0..1 """ """ set volume level, range 0..1. """
self.cast.set_volume(volume) 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): def media_play(self):
""" Service to send the chromecast the command for play/pause. """ """ Send play commmand. """
if self.media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED): self.cast.media_controller.play()
self.cast.media_controller.play()
def media_pause(self): def media_pause(self):
""" Service to send the chromecast the command for play/pause. """ """ Send pause command. """
if self.media_state == MEDIA_STATE_PLAYING: self.cast.media_controller.pause()
self.cast.media_controller.pause()
def media_prev_track(self): def media_previous_track(self):
""" media_prev_track media player. """ """ Send previous track command. """
self.cast.media_controller.rewind() self.cast.media_controller.rewind()
def media_next_track(self): def media_next_track(self):
""" media_next_track media player. """ """ Send next track command. """
self.cast.media_controller.skip() self.cast.media_controller.skip()
def play_youtube_video(self, video_id): def media_seek(self, position):
""" Plays specified video_id on the Chromecast's YouTube channel. """ """ Seek the media to a specific location. """
self.youtube.play_video(video_id) 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): def new_cast_status(self, status):
""" Called when a new cast status is received. """ """ Called when a new cast status is received. """

View File

@ -5,121 +5,334 @@ homeassistant.components.media_player.demo
Demo implementation of the media player. Demo implementation of the media player.
""" """
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED) SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
from homeassistant.const import ATTR_ENTITY_PICTURE SUPPORT_NEXT_TRACK)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """ """ Sets up the cast platform. """
add_devices([ add_devices([
DemoMediaPlayer( DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44', 'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'), '♥♥ 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): YOUTUBE_PLAYER_SUPPORT = \
""" A Demo media player that only supports YouTube. """ 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._name = name
self.is_playing = youtube_id is not None self._player_state = STATE_PLAYING
self.youtube_id = youtube_id self._volume_level = 1.0
self.media_title = media_title self._volume_muted = False
self.volume = 1.0
self.is_volume_muted = False
@property @property
def should_poll(self): def should_poll(self):
""" No polling needed for a demo componentn. """ """ We will push an update after each command. """
return False return False
@property @property
def name(self): def name(self):
""" Returns the name of the device. """ """ Name of the media player. """
return self._name return self._name
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """ State of the player. """
return STATE_NO_APP if self.youtube_id is None else "YouTube" return self._player_state
@property @property
def state_attributes(self): def volume_level(self):
""" Returns the state attributes. """ """ Volume level of the media player (0..1). """
if self.youtube_id is None: return self._volume_level
return
state_attr = { @property
ATTR_MEDIA_CONTENT_ID: self.youtube_id, def is_volume_muted(self):
ATTR_MEDIA_TITLE: self.media_title, """ Boolean if volume is currently muted. """
ATTR_MEDIA_DURATION: 100, return self._volume_muted
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
def turn_on(self): def turn_on(self):
""" turn_off media player. """ """ turn the media player on. """
self.youtube_id = "eyU3bRy2x44" self._player_state = STATE_PLAYING
self.is_playing = False
self.update_ha_state() self.update_ha_state()
def turn_off(self): def turn_off(self):
""" turn_off media player. """ """ turn the media player off. """
self.youtube_id = None self._player_state = STATE_OFF
self.is_playing = False
self.update_ha_state() self.update_ha_state()
def volume_up(self): def mute_volume(self, mute):
""" volume_up media player. """ """ mute the volume. """
if self.volume < 1: self._volume_muted = mute
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
self.update_ha_state() self.update_ha_state()
def media_play_pause(self): def set_volume_level(self, volume):
""" media_play_pause media player. """ """ set volume level, range 0..1. """
self.is_playing = not self.is_playing self._volume_level = volume
self.update_ha_state() self.update_ha_state()
def media_play(self): def media_play(self):
""" media_play media player. """ """ Send play commmand. """
self.is_playing = True self._player_state = STATE_PLAYING
self.update_ha_state() self.update_ha_state()
def media_pause(self): def media_pause(self):
""" media_pause media player. """ """ Send pause command. """
self.is_playing = False self._player_state = STATE_PAUSED
self.update_ha_state() 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): def play_youtube(self, media_id):
""" Plays a YouTube media. """ """ Plays a YouTube media. """
self.youtube_id = media_id self.youtube_id = media_id
self.media_title = 'Demo media title' self._media_title = 'some YouTube video'
self.is_playing = True
self.update_ha_state() 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()

View File

@ -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()

View File

@ -32,16 +32,28 @@ Location of your Music Player Daemon.
import logging import logging
import socket 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 ( from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, MediaPlayerDevice,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
ATTR_MEDIA_ALBUM, ATTR_MEDIA_DATE, ATTR_MEDIA_DURATION, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PAUSED, MEDIA_STATE_PLAYING, MEDIA_TYPE_MUSIC)
MEDIA_STATE_STOPPED, MEDIA_STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the MPD platform. """ """ Sets up the MPD platform. """
@ -50,10 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get('port', 6600) port = config.get('port', 6600)
location = config.get('location', 'MPD') location = config.get('location', 'MPD')
try: if mpd is None:
from mpd import MPDClient
except ImportError:
_LOGGER.exception( _LOGGER.exception(
"Unable to import mpd2. " "Unable to import mpd2. "
"Did you maybe not install the 'python-mpd2' package?") "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 # pylint: disable=no-member
try: try:
mpd_client = MPDClient() mpd_client = mpd.MPDClient()
mpd_client.connect(daemon, port) mpd_client.connect(daemon, port)
mpd_client.close() mpd_client.close()
mpd_client.disconnect() mpd_client.disconnect()
@ -73,110 +82,112 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
mpd = [] add_devices([MpdDevice(daemon, port, location)])
mpd.append(MpdDevice(daemon, port, location))
add_devices(mpd)
class MpdDevice(MediaPlayerDevice): class MpdDevice(MediaPlayerDevice):
""" Represents a MPD server. """ """ Represents a MPD server. """
def __init__(self, server, port, location): # MPD confuses pylint
from mpd import MPDClient # pylint: disable=no-member, abstract-method
def __init__(self, server, port, location):
self.server = server self.server = server
self.port = port self.port = port
self._name = location 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.timeout = 10
self.client.idletimeout = None 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 @property
def name(self): def name(self):
""" Returns the name of the device. """ """ Returns the name of the device. """
return self._name return self._name
# pylint: disable=no-member
@property @property
def state(self): 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. """ """ Returns the media state. """
media_controller = self.client.status() if self.status['state'] == 'play':
return STATE_PLAYING
if media_controller['state'] == 'play': elif self.status['state'] == 'pause':
return MEDIA_STATE_PLAYING return STATE_PAUSED
elif media_controller['state'] == 'pause':
return MEDIA_STATE_PAUSED
elif media_controller['state'] == 'stop':
return MEDIA_STATE_STOPPED
else: else:
return MEDIA_STATE_UNKNOWN return STATE_OFF
# pylint: disable=no-member
@property @property
def state_attributes(self): def media_content_id(self):
""" Returns the state attributes. """ """ Content ID of current playing media. """
status = self.client.status() return self.currentsong['id']
current_song = self.client.currentsong()
if not status and not current_song: @property
state_attr = {} def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
if current_song['id']: @property
state_attr[ATTR_MEDIA_CONTENT_ID] = current_song['id'] 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']: @property
state_attr[ATTR_MEDIA_DATE] = current_song['date'] def media_title(self):
""" Title of current playing media. """
return self.currentsong['title']
if current_song['title']: @property
state_attr[ATTR_MEDIA_TITLE] = current_song['title'] def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.currentsong.get('artist')
if current_song['time']: @property
state_attr[ATTR_MEDIA_DURATION] = current_song['time'] def media_album_name(self):
""" Album of current playing media. (Music track only) """
return self.currentsong.get('album')
if current_song['artist']: @property
state_attr[ATTR_MEDIA_ARTIST] = current_song['artist'] def volume_level(self):
return int(self.status['volume'])/100
if current_song['album']: @property
state_attr[ATTR_MEDIA_ALBUM] = current_song['album'] def supported_media_commands(self):
""" Flags of media commands that are supported. """
state_attr[ATTR_MEDIA_VOLUME] = status['volume'] return SUPPORT_MPD
return state_attr
def turn_off(self): def turn_off(self):
""" Service to exit the running MPD. """ """ Service to exit the running MPD. """
self.client.stop() self.client.stop()
def set_volume_level(self, volume):
""" Sets volume """
self.client.setvol(int(volume * 100))
def volume_up(self): def volume_up(self):
""" Service to send the MPD the command for volume up. """ """ 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: if current_volume <= 100:
self.client.setvol(int(current_volume) + 5) self.client.setvol(current_volume + 5)
def volume_down(self): def volume_down(self):
""" Service to send the MPD the command for volume down. """ """ 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: if current_volume >= 0:
self.client.setvol(int(current_volume) - 5) self.client.setvol(current_volume - 5)
def media_play_pause(self):
""" Service to send the MPD the command for play/pause. """
self.client.pause()
def media_play(self): def media_play(self):
""" Service to send the MPD the command for play/pause. """ """ 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. """ """ Service to send the MPD the command for next track. """
self.client.next() self.client.next()
def media_prev_track(self): def media_previous_track(self):
""" Service to send the MPD the command for previous track. """ """ Service to send the MPD the command for previous track. """
self.client.previous() self.client.previous()

View File

@ -97,5 +97,5 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) 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 return True

View File

@ -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)

View File

@ -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()

View File

@ -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]

View File

@ -113,7 +113,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Unable to import blockchain. " "Unable to import blockchain. "
"Did you maybe not install the 'blockchain' package?") "Did you maybe not install the 'blockchain' package?")
return None return False
wallet_id = config.get('wallet', None) wallet_id = config.get('wallet', None)
password = config.get('password', None) password = config.get('password', None)

View File

@ -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()

View File

@ -1,7 +1,6 @@
""" """
homeassistant.components.sensor.openweathermap homeassistant.components.sensor.openweathermap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OpenWeatherMap (OWM) service. OpenWeatherMap (OWM) service.
Configuration: Configuration:
@ -12,7 +11,8 @@ following to your config/configuration.yaml
sensor: sensor:
platform: openweathermap platform: openweathermap
api_key: YOUR_APP_KEY api_key: YOUR_APP_KEY
monitored_variables: forecast: 0 or 1
monitored_conditions:
- weather - weather
- temperature - temperature
- wind_speed - wind_speed
@ -28,15 +28,13 @@ api_key
*Required *Required
To retrieve this value log into your account at http://openweathermap.org/ 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 monitored_conditions
*Required *Optional
An array specifying the variables to monitor. Conditions to monitor. See the configuration example above for a
These are the variables for the monitored_conditions array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
list of all available conditions to monitor. list of all available conditions to monitor.
Details for the API : http://bugs.openweathermap.org/projects/api/wiki 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. " "Unable to import pyowm. "
"Did you maybe not install the 'PyOWM' package?") "Did you maybe not install the 'PyOWM' package?")
return None return False
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit unit = hass.config.temperature_unit
forecast = config.get('forecast', 0)
owm = OWM(config.get(CONF_API_KEY, None)) owm = OWM(config.get(CONF_API_KEY, None))
if not owm: if not owm:
@ -93,13 +92,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Please check your settings for OpenWeatherMap.") "Please check your settings for OpenWeatherMap.")
return None return None
data = WeatherData(owm, hass.config.latitude, hass.config.longitude) data = WeatherData(owm, forecast, hass.config.latitude,
hass.config.longitude)
dev = [] dev = []
for variable in config['monitored_conditions']: try:
if variable not in SENSOR_TYPES: for variable in config['monitored_conditions']:
_LOGGER.error('Sensor type: "%s" does not exist', variable) if variable not in SENSOR_TYPES:
else: _LOGGER.error('Sensor type: "%s" does not exist', variable)
dev.append(OpenWeatherMapSensor(data, variable, unit)) 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) add_devices(dev)
@ -108,11 +115,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class OpenWeatherMapSensor(Entity): class OpenWeatherMapSensor(Entity):
""" Implements an OpenWeatherMap sensor. """ """ Implements an OpenWeatherMap sensor. """
def __init__(self, weather_data, sensor_type, unit): def __init__(self, weather_data, sensor_type, temp_unit):
self.client_name = 'Weather - ' self.client_name = 'Weather'
self._name = SENSOR_TYPES[sensor_type][0] self._name = SENSOR_TYPES[sensor_type][0]
self.owa_client = weather_data self.owa_client = weather_data
self._unit = unit self.temp_unit = temp_unit
self.type = sensor_type self.type = sensor_type
self._state = None self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -120,7 +127,7 @@ class OpenWeatherMapSensor(Entity):
@property @property
def name(self): def name(self):
return self.client_name + ' ' + self._name return '{} {}'.format(self.client_name, self._name)
@property @property
def state(self): def state(self):
@ -138,14 +145,15 @@ class OpenWeatherMapSensor(Entity):
self.owa_client.update() self.owa_client.update()
data = self.owa_client.data data = self.owa_client.data
fc_data = self.owa_client.fc_data
if self.type == 'weather': if self.type == 'weather':
self._state = data.get_detailed_status() self._state = data.get_detailed_status()
elif self.type == 'temperature': elif self.type == 'temperature':
if self._unit == TEMP_CELCIUS: if self.temp_unit == TEMP_CELCIUS:
self._state = round(data.get_temperature('celsius')['temp'], self._state = round(data.get_temperature('celsius')['temp'],
1) 1)
elif self._unit == TEMP_FAHRENHEIT: elif self.temp_unit == TEMP_FAHRENHEIT:
self._state = round(data.get_temperature('fahrenheit')['temp'], self._state = round(data.get_temperature('fahrenheit')['temp'],
1) 1)
else: else:
@ -161,29 +169,39 @@ class OpenWeatherMapSensor(Entity):
elif self.type == 'rain': elif self.type == 'rain':
if data.get_rain(): if data.get_rain():
self._state = round(data.get_rain()['3h'], 0) self._state = round(data.get_rain()['3h'], 0)
self._unit_of_measurement = 'mm'
else: else:
self._state = 'not raining' self._state = 'not raining'
self._unit_of_measurement = '' self._unit_of_measurement = ''
elif self.type == 'snow': elif self.type == 'snow':
if data.get_snow(): if data.get_snow():
self._state = round(data.get_snow(), 0) self._state = round(data.get_snow(), 0)
self._unit_of_measurement = 'mm'
else: else:
self._state = 'not snowing' self._state = 'not snowing'
self._unit_of_measurement = '' self._unit_of_measurement = ''
elif self.type == 'forecast':
self._state = fc_data.get_weathers()[0].get_status()
class WeatherData(object): class WeatherData(object):
""" Gets the latest data from OpenWeatherMap. """ """ Gets the latest data from OpenWeatherMap. """
def __init__(self, owm, latitude, longitude): def __init__(self, owm, forecast, latitude, longitude):
self.owm = owm self.owm = owm
self.forecast = forecast
self.latitude = latitude self.latitude = latitude
self.longitude = longitude self.longitude = longitude
self.data = None self.data = None
self.fc_data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
""" Gets the latest data from OpenWeatherMap. """ """ Gets the latest data from OpenWeatherMap. """
obs = self.owm.weather_at_coords(self.latitude, self.longitude) obs = self.owm.weather_at_coords(self.latitude, self.longitude)
self.data = obs.get_weather() 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()

View File

@ -122,7 +122,7 @@ class PublicTransportData(object):
try: try:
return [ return [
dt_util.datetime_to_short_time_str( dt_util.datetime_to_time_str(
dt_util.as_local(dt_util.utc_from_timestamp( dt_util.as_local(dt_util.utc_from_timestamp(
item['from']['departureTimestamp'])) item['from']['departureTimestamp']))
) )

View File

@ -21,9 +21,26 @@ sensor:
- type: 'memory_use_percent' - type: 'memory_use_percent'
- type: 'memory_use' - type: 'memory_use'
- type: 'memory_free' - 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: 'processor_use'
- type: 'process' - type: 'process'
arg: 'octave-cli' arg: 'octave-cli'
- type: 'last_boot'
- type: 'since_last_boot'
Variables: Variables:
@ -42,12 +59,12 @@ arg
*Optional *Optional
Additional details for the type, eg. path, binary name, etc. 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.helpers.entity import Entity
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
import psutil
import logging
SENSOR_TYPES = { SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'], 'disk_use_percent': ['Disk Use', '%'],
@ -58,6 +75,17 @@ SENSOR_TYPES = {
'memory_free': ['RAM Free', 'MiB'], 'memory_free': ['RAM Free', 'MiB'],
'processor_use': ['CPU Use', '%'], 'processor_use': ['CPU Use', '%'],
'process': ['Process', ''], '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__) _LOGGER = logging.getLogger(__name__)
@ -103,6 +131,7 @@ class SystemMonitorSensor(Entity):
def unit_of_measurement(self): def unit_of_measurement(self):
return self._unit_of_measurement return self._unit_of_measurement
# pylint: disable=too-many-branches
def update(self): def update(self):
if self.type == 'disk_use_percent': if self.type == 'disk_use_percent':
self._state = psutil.disk_usage(self.argument).percent self._state = psutil.disk_usage(self.argument).percent
@ -120,6 +149,12 @@ class SystemMonitorSensor(Entity):
1024**2, 1) 1024**2, 1)
elif self.type == 'memory_free': elif self.type == 'memory_free':
self._state = round(psutil.virtual_memory().available / 1024**2, 1) 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': elif self.type == 'processor_use':
self._state = round(psutil.cpu_percent(interval=None)) self._state = round(psutil.cpu_percent(interval=None))
elif self.type == 'process': elif self.type == 'process':
@ -127,3 +162,24 @@ class SystemMonitorSensor(Entity):
self._state = STATE_ON self._state = STATE_ON
else: else:
self._state = STATE_OFF 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())

View File

@ -12,24 +12,18 @@ following to your config/configuration.yaml
sensor: sensor:
platform: time_date platform: time_date
display_options: display_options:
- type: 'time' - 'time'
- type: 'date' - 'date'
- type: 'date_time' - 'date_time'
- type: 'time_date' - 'time_date'
- type: 'time_utc' - 'time_utc'
- type: 'beat' - 'beat'
Variables: Variables:
display_options display_options
*Required *Required
An array specifying the variables to display. The variable you wish to display. See the configuration example above for a
These are the variables for the display_options array.:
type
*Required
The variable you wish to display, see the configuration example above for a
list of all available variables. list of all available variables.
""" """
import logging import logging
@ -57,10 +51,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = [] dev = []
for variable in config['display_options']: for variable in config['display_options']:
if variable['type'] not in OPTION_TYPES: if variable not in OPTION_TYPES:
_LOGGER.error('Option type: "%s" does not exist', variable['type']) _LOGGER.error('Option type: "%s" does not exist', variable)
else: else:
dev.append(TimeDateSensor(variable['type'])) dev.append(TimeDateSensor(variable))
add_devices(dev) add_devices(dev)
@ -89,9 +83,9 @@ class TimeDateSensor(Entity):
""" Gets the latest data and updates the states. """ """ Gets the latest data and updates the states. """
time_date = dt_util.utcnow() time_date = dt_util.utcnow()
time = dt_util.datetime_to_short_time_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_short_time_str(time_date) time_utc = dt_util.datetime_to_time_str(time_date)
date = dt_util.datetime_to_short_date_str(dt_util.as_local(time_date)) date = dt_util.datetime_to_date_str(dt_util.as_local(time_date))
# Calculate the beat (Swatch Internet Time) time without date. # Calculate the beat (Swatch Internet Time) time without date.
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':') hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')

View File

@ -7,6 +7,7 @@ import logging
from datetime import timedelta from datetime import timedelta
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -33,6 +34,11 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SWITCHES: 'isy994', isy994.DISCOVER_SWITCHES: 'isy994',
} }
PROP_TO_ATTR = {
'current_power_mwh': ATTR_CURRENT_POWER_MWH,
'today_power_mw': ATTR_TODAY_MWH,
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -74,10 +80,48 @@ def setup(hass, config):
else: else:
switch.turn_off() 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_OFF, handle_switch_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service) hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
return True 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

View File

@ -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)

View File

@ -6,8 +6,7 @@ homeassistant.components.switch.command_switch
Allows to configure custom shell commands to turn a switch on/off. Allows to configure custom shell commands to turn a switch on/off.
""" """
import logging import logging
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
import subprocess import subprocess
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,11 +29,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(devices) add_devices_callback(devices)
class CommandSwitch(ToggleEntity): class CommandSwitch(SwitchDevice):
""" Represents a switch that can be togggled using shell commands """ """ Represents a switch that can be togggled using shell commands """
def __init__(self, name, command_on, command_off): def __init__(self, name, command_on, command_off):
self._name = name or DEVICE_DEFAULT_NAME self._name = name
self._state = STATE_OFF self._state = False
self._command_on = command_on self._command_on = command_on
self._command_off = command_off self._command_off = command_off
@ -60,22 +59,19 @@ class CommandSwitch(ToggleEntity):
""" The name of the switch """ """ The name of the switch """
return self._name return self._name
@property
def state(self):
""" Returns the state of the switch. """
return self._state
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
return self._state == STATE_ON return self._state
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
if CommandSwitch._switch(self._command_on): if CommandSwitch._switch(self._command_on):
self._state = STATE_ON self._state = True
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turn the device off. """ """ Turn the device off. """
if CommandSwitch._switch(self._command_off): if CommandSwitch._switch(self._command_off):
self._state = STATE_OFF self._state = False
self.update_ha_state()

View File

@ -5,20 +5,20 @@ homeassistant.components.switch.demo
Demo platform that has two fake switches. Demo platform that has two fake switches.
""" """
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME from homeassistant.const import DEVICE_DEFAULT_NAME
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo switches. """ """ Find and return demo switches. """
add_devices_callback([ add_devices_callback([
DemoSwitch('Ceiling', STATE_ON), DemoSwitch('Ceiling', True),
DemoSwitch('AC', STATE_OFF) DemoSwitch('AC', False)
]) ])
class DemoSwitch(ToggleEntity): class DemoSwitch(SwitchDevice):
""" Provides a demo switch. """ """ Provides a demo switch. """
def __init__(self, name, state): def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME self._name = name or DEVICE_DEFAULT_NAME
@ -35,19 +35,27 @@ class DemoSwitch(ToggleEntity):
return self._name return self._name
@property @property
def state(self): def current_power_mwh(self):
""" Returns the state of the device if any. """ """ Current power usage in mwh. """
return self._state if self._state:
return 100
@property
def today_power_mw(self):
""" Today total power usage in mw. """
return 1500
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
return self._state == STATE_ON return self._state
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turn the device on. """ """ Turn the device on. """
self._state = STATE_ON self._state = True
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turn the device off. """ """ Turn the device off. """
self._state = STATE_OFF self._state = False
self.update_ha_state()

View File

@ -3,6 +3,12 @@ homeassistant.components.switch.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Tellstick switches. 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 import logging
@ -11,6 +17,8 @@ from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants import tellcore.constants as tellcore_constants
SINGAL_REPETITIONS = 1
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): 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") "Failed to import tellcore")
return return
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
core = telldus.TelldusCore() core = telldus.TelldusCore()
switches_and_lights = core.devices() 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: for switch in switches_and_lights:
if not switch.methods(tellcore_constants.TELLSTICK_DIM): if not switch.methods(tellcore_constants.TELLSTICK_DIM):
switches.append(TellstickSwitchDevice(switch)) switches.append(TellstickSwitchDevice(switch, signal_repetitions))
add_devices_callback(switches) add_devices_callback(switches)
@ -39,9 +49,10 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON | last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF) tellcore_constants.TELLSTICK_TURNOFF)
def __init__(self, tellstick): def __init__(self, tellstick, signal_repetitions):
self.tellstick = tellstick self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name} self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self.signal_repetitions = signal_repetitions
@property @property
def name(self): def name(self):
@ -63,8 +74,10 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
self.tellstick.turn_on() for _ in range(self.signal_repetitions):
self.tellstick.turn_on()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
""" Turns the switch off. """ """ Turns the switch off. """
self.tellstick.turn_off() for _ in range(self.signal_repetitions):
self.tellstick.turn_off()

View File

@ -6,9 +6,7 @@ Support for WeMo switches.
""" """
import logging import logging
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchDevice
from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -43,10 +41,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if isinstance(switch, pywemo.Switch)]) if isinstance(switch, pywemo.Switch)])
class WemoSwitch(ToggleEntity): class WemoSwitch(SwitchDevice):
""" Represents a WeMo switch within Home Assistant. """ """ Represents a WeMo switch within Home Assistant. """
def __init__(self, wemo): def __init__(self, wemo):
self.wemo = wemo self.wemo = wemo
self.insight_params = None
@property @property
def unique_id(self): def unique_id(self):
@ -59,15 +58,16 @@ class WemoSwitch(ToggleEntity):
return self.wemo.name return self.wemo.name
@property @property
def state_attributes(self): def current_power_mwh(self):
""" Returns optional state attributes. """ """ Current power usage in mwh. """
if self.wemo.model.startswith('Belkin Insight'): if self.insight_params:
cur_info = self.wemo.insight_params return self.insight_params['currentpower']
return { @property
ATTR_CURRENT_POWER_MWH: cur_info['currentpower'], def today_power_mw(self):
ATTR_TODAY_MWH: cur_info['todaymw'] """ Today total power usage in mw. """
} if self.insight_params:
return self.insight_params['todaymw']
@property @property
def is_on(self): def is_on(self):
@ -85,3 +85,5 @@ class WemoSwitch(ToggleEntity):
def update(self): def update(self):
""" Update WeMo state. """ """ Update WeMo state. """
self.wemo.get_state(True) self.wemo.get_state(True)
if self.wemo.model.startswith('Belkin Insight'):
self.insight_params = self.wemo.insight_params

View File

@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
def update(self): def update(self):
""" Update state of the light. """ """ Update state of the light. """
self.wink.wait_till_desired_reached() self.wink.updateState()

View File

@ -40,6 +40,9 @@ STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown" STATE_UNKNOWN = "unknown"
STATE_OPEN = 'open' STATE_OPEN = 'open'
STATE_CLOSED = 'closed' STATE_CLOSED = 'closed'
STATE_PLAYING = 'playing'
STATE_PAUSED = 'paused'
STATE_IDLE = 'idle'
# #### STATE AND EVENT ATTRIBUTES #### # #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event # 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_PLAY = "media_play"
SERVICE_MEDIA_PAUSE = "media_pause" SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track" 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 #### # #### API / REMOTE ####
SERVER_PORT = 8123 SERVER_PORT = 8123

View File

@ -9,9 +9,9 @@ import datetime as dt
import pytz import pytz
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y" DATETIME_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_SHORT_STR_FORMAT = "%Y-%m-%d" DATE_STR_FORMAT = "%Y-%m-%d"
TIME_SHORT_STR_FORMAT = "%H:%M" TIME_STR_FORMAT = "%H:%M"
UTC = DEFAULT_TIME_ZONE = pytz.utc UTC = DEFAULT_TIME_ZONE = pytz.utc
@ -34,7 +34,7 @@ def get_time_zone(time_zone_str):
def utcnow(): def utcnow():
""" Get now in UTC time. """ """ Get now in UTC time. """
return dt.datetime.now(pytz.utc) return dt.datetime.now(UTC)
def now(time_zone=None): def now(time_zone=None):
@ -45,12 +45,12 @@ def now(time_zone=None):
def as_utc(dattim): def as_utc(dattim):
""" Return a datetime as UTC time. """ Return a datetime as UTC time.
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """ Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """
if dattim.tzinfo == pytz.utc: if dattim.tzinfo == UTC:
return dattim return dattim
elif dattim.tzinfo is None: elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
return dattim.astimezone(pytz.utc) return dattim.astimezone(UTC)
def as_local(dattim): def as_local(dattim):
@ -58,17 +58,28 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE: if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim return dattim
elif dattim.tzinfo is None: elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=pytz.utc) dattim = dattim.replace(tzinfo=UTC)
return dattim.astimezone(DEFAULT_TIME_ZONE) return dattim.astimezone(DEFAULT_TIME_ZONE)
def utc_from_timestamp(timestamp): def utc_from_timestamp(timestamp):
""" Returns a UTC time from a 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. """ """ Converts datetime to specified time_zone and returns a string. """
return datetime_to_str(as_local(dattim)) 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): def datetime_to_str(dattim):
""" Converts datetime to a string format. """ 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 @rtype : str
""" """
return dattim.strftime(DATE_STR_FORMAT) 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): def str_to_datetime(dt_str):
""" Converts a string to a UTC datetime object. """ Converts a string to a UTC datetime object.
@ -104,7 +115,15 @@ def str_to_datetime(dt_str):
""" """
try: try:
return dt.datetime.strptime( 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 except ValueError: # If dt_str did not match our format
return None return None

View File

@ -18,7 +18,7 @@ phue>=0.8
ledcontroller>=1.0.7 ledcontroller>=1.0.7
# Chromecast bindings (media_player.cast) # Chromecast bindings (media_player.cast)
pychromecast>=0.6.4 pychromecast>=0.6.6
# Keyboard (keyboard) # Keyboard (keyboard)
pyuserinput>=0.1.9 pyuserinput>=0.1.9
@ -39,10 +39,10 @@ python-nest>=2.3.1
pydispatcher>=2.0.5 pydispatcher>=2.0.5
# ISY994 bindings (*.isy994) # ISY994 bindings (*.isy994)
PyISY>=1.0.2 PyISY>=1.0.5
# PSutil (sensor.systemmonitor) # PSutil (sensor.systemmonitor)
psutil>=2.2.1 psutil>=3.0.0
# Pushover bindings (notify.pushover) # Pushover bindings (notify.pushover)
python-pushover>=0.2 python-pushover>=0.2
@ -51,7 +51,7 @@ python-pushover>=0.2
transmissionrpc>=0.11 transmissionrpc>=0.11
# OpenWeatherMap Web API (sensor.openweathermap) # OpenWeatherMap Web API (sensor.openweathermap)
pyowm>=2.2.0 pyowm>=2.2.1
# XMPP Bindings (notify.xmpp) # XMPP Bindings (notify.xmpp)
sleekxmpp>=1.3.1 sleekxmpp>=1.3.1
@ -64,3 +64,18 @@ python-mpd2>=0.5.4
# Hikvision (switch.hikvisioncam) # Hikvision (switch.hikvisioncam)
hikvision>=0.4 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

View File

@ -10,9 +10,10 @@ import unittest
import homeassistant as ha import homeassistant as ha
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, 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 import homeassistant.components.media_player as media_player
from helpers import mock_service from helpers import mock_service
@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase):
self.hass = ha.HomeAssistant() self.hass = ha.HomeAssistant()
self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room') 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.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom')
self.hass.states.set(self.test_entity2, "YouTube") 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_PLAY: media_player.media_play,
SERVICE_MEDIA_PAUSE: media_player.media_pause, SERVICE_MEDIA_PAUSE: media_player.media_pause,
SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track, 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(): for service_name, service_method in services.items():