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

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/).
@ -8,7 +8,7 @@ It offers the following functionality through built-in components:
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index))
* Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/)
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and XBMC/Kodi (http://kodi.tv/)
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/)
* Track running system services and monitoring your system stats (Memory, disk usage, and more)
* Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands

View File

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

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 people leave the house
- offer a service to turn it on for 10 seconds
Configuration:
To use the Example custom component you will need to add the following to
your config/configuration.yaml
example:
target: TARGET_ENTITY
Variable:
target
*Required
TARGET_ENTITY should be one of your devices that can be turned on and off,
ie a light or a switch. Example value could be light.Ceiling or switch.AC
(if you have these devices with those names).
"""
import time
import logging
@ -31,6 +47,7 @@ CONF_TARGET = 'target'
# Name of the service that we expose
SERVICE_FLASH = 'flash'
# Shortcut for the logger
_LOGGER = logging.getLogger(__name__)

View File

@ -3,6 +3,14 @@ custom_components.hello_world
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements the bare minimum that a component should implement.
Configuration:
To use the hello_word component you will need to add the following to your
config/configuration.yaml
hello_world:
"""
# The domain of your component. Should be equal to the name of your component

View File

@ -186,6 +186,24 @@ def from_config_file(config_path, hass=None):
def enable_logging(hass):
""" Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt,
datefmt='%y-%m-%d %H:%M:%S',
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
_LOGGER.warn("Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path('home-assistant.log')
@ -202,7 +220,7 @@ def enable_logging(hass):
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
datefmt='%y-%m-%d %H:%M:%S'))
logging.getLogger('').addHandler(err_handler)
else:

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
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/schoutsen/picture"})
"http://graph.facebook.com/297400035/picture"})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/anne.t.frederiksen/picture"})
"http://graph.facebook.com/621994601/picture"})
hass.states.set("group.all_devices", "home",
{
"auto": True,
ATTR_ENTITY_ID: [
"device_tracker.Paulus",
"device_tracker.Anne_Therese"
"device_tracker.paulus",
"device_tracker.anne_therese"
]
})

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__)
FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
def setup(hass, config):
""" Setup serving the frontend. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
hass.http.register_path('GET', URL_ROOT, _handle_get_root, False)
for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', STATES_URL, _handle_get_root, False)
# Static files
hass.http.register_path(

View File

@ -1,2 +1,2 @@
""" 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-checkbox": "PolymerElements/paper-checkbox#^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",
"moment": "^2.10.3",
"layout": "Polymer/layout",
"color-picker-element": "~0.0.3",
"paper-styles": "polymerelements/paper-styles#~1.0"
"paper-styles": "polymerelements/paper-styles#^1.0.0",
"lodash": "~3.9.3",
"pikaday": "~1.3.2"
},
"resolutions": {
"polymer": "^1.0.0",

View File

@ -33,8 +33,8 @@
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info>
<div class='state'>
<div class='main-text'>[[computePrimaryText(stateObj)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj)]]</div>
<div class='main-text'>[[computePrimaryText(stateObj, isPlaying)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj, isPlaying)]]</div>
</div>
</div>
</template>
@ -42,6 +42,7 @@
<script>
(function() {
var PLAYING_STATES = ['playing', 'paused'];
Polymer({
is: 'state-card-media_player',
@ -49,14 +50,41 @@
stateObj: {
type: Object,
},
isPlaying: {
type: Boolean,
computed: 'computeIsPlaying(stateObj)',
},
},
computePrimaryText: function(stateObj) {
return stateObj.attributes.media_title || stateObj.stateDisplay;
computeIsPlaying: function(stateObj) {
return PLAYING_STATES.indexOf(stateObj.state) !== -1;
},
computeSecondaryText: function(stateObj) {
return stateObj.attributes.media_title ? stateObj.stateDisplay : '';
computePrimaryText: function(stateObj, isPlaying) {
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">
<template>
<template is='dom-if' if=[[allowToggle]]>
<template is='dom-if' if='[[allowToggle]]'>
<state-card-toggle state-obj="[[stateObj]]"></state-card-toggle>
</template>
<template is='dom-if' if=[[!allowToggle]]>
<template is='dom-if' if='[[!allowToggle]]'>
<state-card-display state-obj="[[stateObj]]"></state-card-display>
</template>
</template>

View File

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

View File

@ -20,7 +20,7 @@
<template>
<ul>
<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>
</ul>
</template>
@ -28,25 +28,30 @@
<script>
(function() {
var entityGetters = window.hass.entityGetters;
Polymer({
is: 'entity-list',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
entities: {
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) {
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>
(function() {
var eventGetters = window.hass.eventGetters;
Polymer({
is: 'events-list',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
events: {
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) {
ev.preventDefault();
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>
<template>
<template is='dom-if' if='[[noEntries(entries)]]'>
No logbook entries found.
</template>
<template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry>
</template>
@ -27,6 +30,10 @@
value: [],
},
},
noEntries: function(entries) {
return !entries.length;
}
});
})();
</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>
(function() {
var uiActions = window.hass.uiActions;
var moreInfoActions = window.hass.moreInfoActions;
Polymer({
is: 'logbook-entry',
entityClicked: function(ev) {
ev.preventDefault();
uiActions.showMoreInfoDialog(this.entryObj.entityId);
moreInfoActions.selectEntity(this.entryObj.entityId);
}
});

View File

@ -23,10 +23,10 @@
<template>
<ul>
<template is='dom-repeat' items="[[domains]]" as="domain">
<template is='dom-repeat' items="[[computeServices(domain)]]" as="service">
<template is='dom-repeat' items="[[serviceDomains]]" as="domain">
<template is='dom-repeat' items="[[domain.services]]" as="service">
<li><a href='#' on-click='serviceClicked'>
<span>[[domain]]</span>/<span>[[service]]</span>
<span>[[domain.domain]]</span>/<span>[[service]]</span>
</a></li>
</template>
</template>
@ -36,19 +36,24 @@
<script>
(function() {
var serviceGetters = window.hass.serviceGetters;
Polymer({
is: 'services-list',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
domains: {
serviceDomains: {
type: Array,
value: [],
},
services: {
type: Object,
bindNuclear: [
serviceGetters.entityMap,
function(map) {
return map.valueSeq()
.sortBy(function(domain) { return domain.domain; })
.toJS();
},
],
},
},
@ -56,15 +61,10 @@
return this.services.get(domain).toArray();
},
serviceStoreChanged: function(serviceStore) {
this.services = serviceStore.all;
this.domains = this.services.keySeq().sort().toArray();
},
serviceClicked: function(ev) {
ev.preventDefault();
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;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
padding: 16px;
margin: 16px auto;
padding: 0 16px 8px;
margin: 16px;
}
</style>
@ -44,9 +44,15 @@
<state-card class="state-card" state-obj="[[item]]"></state-card>
</template>
<template if="[[computeEmptyStates(states)]]">
<template is='dom-if' if="[[computeEmptyStates(states)]]">
<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>
</template>

View File

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

View File

@ -1,5 +1,14 @@
<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>
(function() {
Polymer({
@ -18,10 +27,6 @@
},
},
created: function() {
this.style.display = 'block';
},
attached: function() {
this.isAttached = true;
},
@ -34,18 +39,17 @@
if (!this.isAttached) {
return;
}
var root = Polymer.dom(this);
var stateHistory = this.data;
while (root.lastChild) {
root.removeChild(root.lastChild);
while (root.node.lastChild) {
root.node.removeChild(root.node.lastChild);
}
if (!stateHistory || stateHistory.length === 0) {
return;
}
// debugger;
var chart = new google.visualization.Timeline(this);
var dataTable = new google.visualization.DataTable();
@ -59,14 +63,19 @@
dataTable.addRow([entityDisplay, stateStr, start, end]);
};
// people can pass in history of 1 entityId or a collection.
// var stateHistory;
// if (_.isArray(data[0])) {
// stateHistory = data;
// } else {
// stateHistory = [data];
// isSingleDevice = true;
// }
var startTime = new Date(
stateHistory.reduce(function(minTime, stateInfo) {
return Math.min(
minTime, stateInfo[0].lastChangedAsDate);
}, new Date())
);
// 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;
// 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++;
}.bind(this));
chart.draw(dataTable, {
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: {
showRowLabels: stateHistory.length > 1
},

View File

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

View File

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

View File

@ -31,7 +31,7 @@
</style>
<template>
<!-- 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>
<div>
<template is='dom-if' if="[[showHistoryComponent]]">
@ -49,9 +49,13 @@
<script>
(function() {
var stateStore = window.hass.stateStore;
var stateHistoryStore = window.hass.stateHistoryStore;
var stateHistoryActions = window.hass.stateHistoryActions;
var configGetters = window.hass.configGetters;
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
var DOMAINS_WITH_NO_HISTORY = ['camera'];
@ -59,29 +63,40 @@
Polymer({
is: 'more-info-dialog',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
entityId: {
type: String,
},
stateObj: {
type: Object,
bindNuclear: moreInfoGetters.currentEntity,
observer: 'stateObjChanged',
},
stateHistory: {
type: Object,
bindNuclear: [
moreInfoGetters.currentEntityHistory,
function(history) {
return history ? [history] : false;
},
],
},
isLoadingHistoryData: {
type: Boolean,
value: false,
bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
},
hasHistoryComponent: {
type: Boolean,
value: false,
bindNuclear: configGetters.isComponentLoaded('history'),
observer: 'fetchHistoryData',
},
shouldFetchHistory: {
type: Boolean,
bindNuclear: moreInfoGetters.isCurrentEntityHistoryStale,
observer: 'fetchHistoryData',
},
showHistoryComponent: {
@ -92,23 +107,14 @@
dialogOpen: {
type: Boolean,
value: false,
observer: 'dialogOpenChanged',
},
},
listeners: {
'iron-overlay-opened': 'onIronOverlayOpened',
'iron-overlay-closed': 'onIronOverlayClosed'
},
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;
fetchHistoryData: function() {
if (this.stateObj && this.hasHistoryComponent &&
this.shouldFetchHistory) {
entityHistoryActions.fetchRecent(this.stateObj.entityId);
}
if(this.stateObj) {
if(DOMAINS_WITH_NO_HISTORY.indexOf(this.stateObj.domain) !== -1) {
@ -120,49 +126,26 @@
}
},
stateHistoryStoreChanged: function() {
var newHistory;
if (this.hasHistoryComponent && this.entityId) {
newHistory = [stateHistoryStore.get(this.entityId)];
} else {
newHistory = null;
stateObjChanged: function(newVal) {
if (!newVal) {
this.dialogOpen = false;
return;
}
this.isLoadingHistoryData = false;
this.fetchHistoryData();
if (newHistory !== this.stateHistory) {
this.stateHistory = newHistory;
// allow dialog to render content before showing it so it is
// 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>

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

View File

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

View File

@ -2,13 +2,6 @@
<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-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-logbook.html'>
@ -18,115 +11,22 @@
<link rel='import' href='../layouts/partial-dev-set-state.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'>
<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>
<notification-manager></notification-manager>
<modal-manager></modal-manager>
<more-info-dialog></more-info-dialog>
<paper-drawer-panel id='drawer' narrow='{{narrow}}'>
<paper-header-panel mode='scroll' drawer class='sidenav fit'>
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div title>Home Assistant</div>
</paper-toolbar>
<ha-sidebar drawer></ha-sidebar>
<paper-menu id='menu' class='layout vertical fit'
selectable='[data-panel]' attr-for-selected='data-panel'
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]]'>
<template is='dom-if' if='[[isSelectedStates]]'>
<partial-states main narrow='[[narrow]]'>
</partial-states>
</template>
<template is='dom-if' if='[[isSelectedLogbook]]'>
<partial-logbook main narrow='[[narrow]]'></partial-logbook>
</template>
@ -149,192 +49,83 @@
<script>
(function() {
var configGetters = window.hass.configGetters;
var entityGetters = window.hass.entityGetters;
var navigationGetters = window.hass.navigationGetters;
var authActions = window.hass.authActions;
var navigationActions = window.hass.navigationActions;
var uiUtil = window.hass.uiUtil;
var uiConstants = window.hass.uiConstants;
var entityDomainFilters = window.hass.util.entityDomainFilters;
var urlSync = window.hass.urlSync;
Polymer({
is: 'home-assistant-main',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
selected: {
type: String,
value: 'states',
},
stateFilter: {
type: String,
value: null,
},
narrow: {
type: Boolean,
},
activeFilters: {
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: {
activePage: {
type: String,
value: 'history',
readOnly: true,
bindNuclear: navigationGetters.activePage,
observer: 'activePageChanged',
},
isSelectedStates: {
type: Boolean,
bindNuclear: navigationGetters.isActivePane('states'),
},
isSelectedHistory: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedHistory)',
},
selectedLogbook: {
type: String,
value: 'logbook',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('history'),
},
isSelectedLogbook: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedLogbook)',
},
selectedDevEvent: {
type: String,
value: 'devEvent',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('logbook'),
},
isSelectedDevEvent: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevEvent)',
},
selectedDevState: {
type: String,
value: 'devState',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('devEvent'),
},
isSelectedDevState: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevState)',
},
selectedDevService: {
type: String,
value: 'devService',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('devState'),
},
isSelectedDevService: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevService)',
bindNuclear: navigationGetters.isActivePane('devService'),
},
},
listeners: {
'menu.core-select': 'menuSelect',
'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() {
this.$.drawer.openDrawer();
},
closeDrawer: function() {
activePageChanged: function() {
this.$.drawer.closeDrawer();
},
handleLogOut: function() {
authActions.logOut();
attached: function() {
urlSync.startSync();
},
computeIsSelected: function(selected, selectedType) {
return selected === selectedType;
detached: function() {
urlSync.stopSync();
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return uiConstants.STATE_FILTERS[filter];
},
filterType: function(filter) {
return 'states_' + filter;
}
});
})();
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,24 @@
<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-input/paper-input.html">
<link rel="import" href="./partial-base.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">
<style>
.content {
background-color: white;
padding: 8px;
}
.selected-date-container {
padding: 0 16px;
}
paper-input {
max-width: 200px;
}
</style>
<template>
<partial-base narrow="[[narrow]]">
@ -20,20 +27,30 @@
<paper-icon-button icon="refresh" header-buttons
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>
</template>
</dom-module>
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn;
var logbookGetters = window.hass.logbookGetters;
var logbookActions = window.hass.logbookActions;
var uiActions = window.hass.uiActions;
var dateToStr = window.hass.util.dateToStr;
Polymer({
is: 'partial-logbook',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
narrow: {
@ -41,22 +58,61 @@
value: false,
},
selectedDate: {
type: String,
bindNuclear: logbookGetters.currentDate,
},
isLoading: {
type: Boolean,
bindNuclear: logbookGetters.isLoadingEntries,
},
isStale: {
type: Boolean,
bindNuclear: logbookGetters.isCurrentStale,
observer: 'isStaleChanged',
},
entries: {
type: Array,
value: [],
bindNuclear: [
logbookGetters.currentEntries,
function(entries) { return entries.toArray(); },
],
},
datePicker: {
type: Object,
},
},
logbookStoreChanged: function(logbookStore) {
if (logbookStore.isStale()) {
logbookActions.fetch();
isStaleChanged: function(newVal) {
if (newVal) {
// isLoading wouldn't update without async <_<
this.async(
function() { logbookActions.fetchDate(this.selectedDate); }, 10);
}
this.entries = logbookStore.all.toArray();
},
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/iron-icon/iron-icon.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="../components/state-cards.html">
<link rel="import" href="../components/ha-voice-command-progress.html">
<dom-module id="partial-states">
<style>
@ -41,21 +41,24 @@
<template>
<partial-base narrow="[[narrow]]">
<span header-title>{{headerTitle}}</span>
<span header-title>[[computeHeaderTitle(filter)]]</span>
<span header-buttons>
<paper-icon-button icon="refresh" class$="[[computeRefreshButtonClass(isFetching)]]"
on-click="handleRefresh" hidden$="[[isStreaming]]"></paper-icon-button>
<paper-icon-button icon="[[listenButtonIcon]]" hidden$={{!canListen}}
<paper-icon-button
icon="refresh"
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>
</span>
<div class='content-wrapper'>
<div class='listening' hidden$="[[!showListenInterface]]"
on-click="handleListenClick">
<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>
<ha-voice-command-progress></ha-voice-command-progress>
</div>
<state-cards states="[[states]]">
@ -75,28 +78,24 @@
<script>
(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 voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var uiConstants = window.hass.uiConstants;
var entityDomainFilters = window.hass.util.entityDomainFilters;
Polymer({
is: 'partial-states',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
/**
* Title to show in the header
*/
headerTitle: {
type: String,
value: 'States',
},
/**
* If header is to be shown in narrow mode.
*/
narrow: {
type: Boolean,
value: false,
@ -104,110 +103,56 @@
filter: {
type: String,
value: null,
observer: 'filterChanged',
},
voiceSupported: {
type: Boolean,
value: voiceActions.isSupported(),
bindNuclear: navigationGetters.activeFilter,
},
isFetching: {
type: Boolean,
value: false,
bindNuclear: syncGetters.isFetching,
},
isStreaming: {
type: Boolean,
value: false,
bindNuclear: streamGetters.isStreamingEvents,
},
canListen: {
type: Boolean,
value: false,
bindNuclear: [
voiceGetters.isVoiceSupported,
configGetters.isComponentLoaded('conversation'),
function(isVoiceSupported, componentLoaded) {
return isVoiceSupported && componentLoaded;
}
]
},
isListening: {
type: Boolean,
value: false,
},
isTransmitting: {
type: Boolean,
value: false,
},
interimTranscript: {
type: String,
value: '',
},
finalTranscript: {
type: String,
value: '',
},
listenButtonIcon: {
type: String,
computed: 'computeListenButtonIcon(isListening)'
bindNuclear: voiceGetters.isListening,
},
showListenInterface: {
type: Boolean,
computed: 'computeShowListenInterface(isListening,isTransmitting)'
}
},
bindNuclear: [
voiceGetters.isListening,
voiceGetters.isTransmitting,
function(isListening, isTransmitting) {
return isListening || isTransmitting;
},
],
},
componentStoreChanged: function(componentStore) {
this.canListen = this.voiceSupported &&
componentStore.isLoaded('conversation');
},
stateStoreChanged: function() {
this.refreshStates();
},
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;});
states: {
type: Array,
bindNuclear: [
navigationGetters.filteredStates,
// are here so a change to services causes a re-render.
// we need this to decide if we show toggles for states.
serviceGetters.entityMap,
function(states) { return states.toArray(); },
],
},
},
handleRefresh: function() {
@ -222,12 +167,12 @@
}
},
computeListenButtonIcon: function(isListening) {
return isListening ? 'av:mic-off' : 'av:mic';
computeHeaderTitle: function(filter) {
return filter ? entityDomainFilters[filter] : 'States';
},
computeShowListenInterface: function(isListening,isTransmitting) {
return isListening || isTransmitting;
computeListenButtonIcon: function(isListening) {
return isListening ? 'av:mic-off' : 'av:mic';
},
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>
(function() {
var notificationGetters = window.hass.notificationGetters;
Polymer({
is: 'notification-manager',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
text: {
type: String,
value: '',
},
lastId: {
type: Number,
bindNuclear: notificationGetters.lastNotificationMessage,
observer: 'showNotification',
},
},
notificationStoreChanged: function(notificationStore) {
if (notificationStore.hasNewNotifications(this.lastId)) {
var notification = notificationStore.lastNotification;
this.lastId = notification.id;
this.text = notification.message;
showNotification: function(newText) {
if (newText) {
this.$.toast.show();
}
},
}
});
})();
</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>
(function() {
var streamGetters = window.hass.streamGetters;
var syncActions = window.hass.syncActions;
var serviceActions = window.hass.serviceActions;
Polymer({
is: 'more-info-configurator',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
stateObj: {
@ -72,7 +73,7 @@
isStreaming: {
type: Boolean,
value: false,
bindNuclear: streamGetters.isStreamingEvents,
},
isConfigurable: {
@ -99,10 +100,6 @@
return stateObj.attributes.submit_caption || 'Set configuration';
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
submitClicked: function() {
this.isConfiguring = true;

View File

@ -23,28 +23,36 @@
<script>
(function() {
var stateStore = window.hass.stateStore;
var entityGetters = window.hass.entityGetters;
var moreInfoGetters = window.hass.moreInfoGetters;
Polymer({
is: 'more-info-group',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
stateObj: {
type: Object,
observer: 'updateStates',
},
states: {
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() {
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/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'>
<style>
@ -13,7 +13,7 @@
transition: max-height .5s ease-in;
}
color-picker {
ha-color-picker {
display: block;
width: 350px;
margin: 0 auto;
@ -27,7 +27,7 @@
max-height: 40px;
}
.has-xy_color color-picker {
.has-xy_color ha-color-picker {
max-height: 500px;
}
</style>
@ -41,7 +41,7 @@
</paper-slider>
</div>
<color-picker on-colorselected='colorPicked' width='350' height='200'>
<ha-color-picker on-colorselected='colorPicked' width='350' height='200'>
</color-picker>
</div>
</template>
@ -73,7 +73,7 @@
this.brightnessSliderValue = newVal.attributes.brightness;
}
this.debounce('more-info-light-animation-finish', function() {
this.async(function() {
this.fire('iron-resize');
}.bind(this), 500);
},

View File

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

View File

@ -41,7 +41,7 @@
<script>
(function() {
var constants = window.hass.constants;
var temperatureUnits = window.hass.util.temperatureUnits;
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['away_mode'];
@ -76,7 +76,8 @@
this.targetTemperatureSliderValue = this.stateObj.state;
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.tempMax = 95;
} else {

View File

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

View File

@ -10,52 +10,16 @@
'sensor',
];
// Add some frontend specific helpers to the models
Object.defineProperties(window.hass.stateModel.prototype, {
// how to render the card for this state
cardType: {
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',
},
};
var reactor = window.hass.reactor;
var serviceGetters = window.hass.serviceGetters;
var authActions = window.hass.authActions;
var preferences = window.hass.localStoragePreferences;
window.hass.uiActions = {
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: window.hass.uiConstants.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
validateAuth: function(authToken, rememberLogin) {
validateAuth: function(authToken, rememberAuth) {
authActions.validate(authToken, {
useStreaming: preferenceStore.useStreaming,
rememberLogin: rememberLogin,
rememberAuth: rememberAuth,
useStreaming: preferences.useStreaming,
});
},
};
@ -65,7 +29,7 @@
stateCardType: function(state) {
if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) {
return state.domain;
} else if(state.canToggle) {
} else if(reactor.evaluate(serviceGetters.canToggle(state.entityId))) {
return "toggle";
} else {
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.
-->
<script src="../bower_components/moment/moment.js"></script>
<script src="../bower_components/moment/min/moment.min.js"></script>
<script>
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>
(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() {
StoreListenerMixIn.listenToStores(true, this);
},
// console.log(key, getter);
detached: function() {
StoreListenerMixIn.stopListeningToStores(this);
},
component[key] = reactor.evaluate(getter);
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>

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

View File

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

View File

@ -8,7 +8,7 @@ import logging
from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_PLAY_PAUSE)
@ -43,7 +43,7 @@ def media_next_track(hass):
def media_prev_track(hass):
""" Press the keyboard button for prev track. """
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK)
def setup(hass, config):
@ -79,7 +79,7 @@ def setup(hass, config):
lambda service:
keyboard.tap_key(keyboard.media_next_track_key))
hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK,
hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_prev_track_key))

View File

@ -53,6 +53,7 @@ import os
import csv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util
from homeassistant.const import (
@ -87,6 +88,10 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
# Apply an effect to the light, can be EFFECT_COLORLOOP
ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop"
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
@ -96,6 +101,11 @@ DISCOVERY_PLATFORMS = {
discovery.services.PHILIPS_HUE: 'hue',
}
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_xy': ATTR_XY_COLOR,
}
_LOGGER = logging.getLogger(__name__)
@ -108,7 +118,8 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, profile=None, flash=None):
rgb_color=None, xy_color=None, profile=None, flash=None,
effect=None):
""" Turns all or specified light on. """
data = {
key: value for key, value in [
@ -119,6 +130,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
] if value is not None
}
@ -247,11 +259,16 @@ def setup(hass, config):
elif dat[ATTR_FLASH] == FLASH_LONG:
params[ATTR_FLASH] = FLASH_LONG
if ATTR_EFFECT in dat:
if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
params[ATTR_EFFECT] = EFFECT_COLORLOOP
for light in target_lights:
light.turn_on(**params)
for light in target_lights:
light.update_ha_state(True)
if light.should_poll:
light.update_ha_state(True)
# Listen for light on and light off service calls
hass.services.register(DOMAIN, SERVICE_TURN_ON,
@ -261,3 +278,41 @@ def setup(hass, config):
handle_light_service)
return True
class Light(ToggleEntity):
""" Represents a light within Home Assistant. """
# pylint: disable=no-self-use
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return None
@property
def color_xy(self):
""" XY color value [float, float]. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
if self.is_on:
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,14 @@ homeassistant.components.logbook
Parses events and generates a human log.
"""
from datetime import timedelta
from itertools import groupby
import re
from homeassistant import State, DOMAIN as HA_DOMAIN
from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
@ -17,12 +19,10 @@ import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = '/api/logbook'
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
ORDER BY time_fired
"""
GROUP_BY_MINUTES = 15
@ -37,11 +37,26 @@ def setup(hass, config):
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
start_today = dt_util.now().replace(hour=0, minute=0, second=0)
date_str = path_match.group('date')
handler.write_json(humanify(
recorder.query_events(
QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),))))
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_day = dt_util.start_of_local_day(start_date)
else:
start_day = dt_util.start_of_local_day()
end_day = start_day + timedelta(days=1)
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
handler.write_json(humanify(events))
class Entry(object):

View File

@ -10,11 +10,12 @@ from homeassistant.components import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING,
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
DOMAIN = 'media_player'
DEPENDENCIES = []
@ -28,28 +29,70 @@ DISCOVERY_PLATFORMS = {
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
STATE_NO_APP = 'idle'
ATTR_STATE = 'state'
ATTR_OPTIONS = 'options'
ATTR_MEDIA_STATE = 'media_state'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM = 'media_album'
ATTR_MEDIA_IMAGE_URL = 'media_image_url'
ATTR_MEDIA_VOLUME = 'media_volume'
ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_DATE = 'media_date'
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
ATTR_MEDIA_TRACK = 'media_track'
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
ATTR_MEDIA_SEASON = 'media_season'
ATTR_MEDIA_EPISODE = 'media_episode'
ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
MEDIA_STATE_UNKNOWN = 'unknown'
MEDIA_STATE_PLAYING = 'playing'
MEDIA_STATE_PAUSED = 'paused'
MEDIA_STATE_STOPPED = 'stopped'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
MEDIA_TYPE_VIDEO = 'movie'
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4
SUPPORT_VOLUME_MUTE = 8
SUPPORT_PREVIOUS_TRACK = 16
SUPPORT_NEXT_TRACK = 32
SUPPORT_YOUTUBE = 64
SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256
YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg'
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
SERVICE_MEDIA_PLAY: 'media_play',
SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
}
ATTR_TO_PROPERTY = [
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_TRACK,
ATTR_MEDIA_SERIES_TITLE,
ATTR_MEDIA_SEASON,
ATTR_MEDIA_EPISODE,
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_SUPPORTED_MEDIA_COMMANDS,
]
def is_on(hass, entity_id=None):
@ -58,7 +101,7 @@ def is_on(hass, entity_id=None):
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
return any(not hass.states.is_state(entity_id, STATE_NO_APP)
return any(not hass.states.is_state(entity_id, STATE_OFF)
for entity_id in entity_ids)
@ -90,21 +133,22 @@ def volume_down(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
def volume_mute(hass, entity_id=None):
""" Send the media player the command to toggle its mute state. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
def mute_volume(hass, mute, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_MUTED: mute}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data)
def volume_set(hass, entity_id=None, volume=None):
""" Set volume on media player. """
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_MEDIA_VOLUME, volume),
] if value is not None
}
def set_volume_level(hass, volume, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data)
@ -137,24 +181,11 @@ def media_next_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
def media_prev_track(hass, entity_id=None):
def media_previous_track(hass, entity_id=None):
""" Send the media player the command for prev track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
SERVICE_MEDIA_PLAY: 'media_play',
SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREV_TRACK: 'media_prev_track',
}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
def setup(hass, config):
@ -180,35 +211,56 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler)
def volume_set_service(service, volume):
def volume_set_service(service):
""" Set specified volume on the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_LEVEL not in service.data:
return
volume = service.data[ATTR_MEDIA_VOLUME_LEVEL]
for player in target_players:
player.volume_set(volume)
player.set_volume_level(volume)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET,
lambda service:
volume_set_service(
service, service.data.get('volume')))
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
def volume_mute_service(service, mute):
def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_MUTED not in service.data:
return
mute = service.data[ATTR_MEDIA_VOLUME_MUTED]
for player in target_players:
player.volume_mute(mute)
player.mute_volume(mute)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service:
volume_mute_service(
service, service.data.get('mute')))
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
def media_seek_service(service):
""" Seek to a position. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_SEEK_POSITION not in service.data:
return
position = service.data[ATTR_MEDIA_SEEK_POSITION]
for player in target_players:
player.seek(position)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
def play_youtube_video_service(service, media_id):
""" Plays specified media_id on the media player. """
@ -239,51 +291,217 @@ def setup(hass, config):
class MediaPlayerDevice(Entity):
""" ABC for media player devices. """
# pylint: disable=too-many-public-methods,no-self-use
# Implement these for your media player
@property
def state(self):
""" State of the player. """
return STATE_UNKNOWN
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return None
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return None
@property
def media_content_id(self):
""" Content ID of current playing media. """
return None
@property
def media_content_type(self):
""" Content type of current playing media. """
return None
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return None
@property
def media_image_url(self):
""" Image url of current playing media. """
return None
@property
def media_title(self):
""" Title of current playing media. """
return None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return None
@property
def media_album_name(self):
""" Album name of current playing media. (Music track only) """
return None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return None
@property
def app_id(self):
""" ID of the current running app. """
return None
@property
def app_name(self):
""" Name of the current running app. """
return None
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return 0
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
return None
def turn_on(self):
""" turn media player on. """
pass
""" turn the media player on. """
raise NotImplementedError()
def turn_off(self):
""" turn media player off. """
pass
""" turn the media player off. """
raise NotImplementedError()
def volume_up(self):
""" volume_up media player. """
pass
def mute_volume(self, mute):
""" mute the volume. """
raise NotImplementedError()
def volume_down(self):
""" volume_down media player. """
pass
def volume_mute(self, mute):
""" mute (true) or unmute (false) media player. """
pass
def volume_set(self, volume):
""" set volume level of media player. """
pass
def media_play_pause(self):
""" media_play_pause media player. """
pass
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
raise NotImplementedError()
def media_play(self):
""" media_play media player. """
pass
""" Send play commmand. """
raise NotImplementedError()
def media_pause(self):
""" media_pause media player. """
pass
""" Send pause command. """
raise NotImplementedError()
def media_prev_track(self):
""" media_prev_track media player. """
pass
def media_previous_track(self):
""" Send previous track command. """
raise NotImplementedError()
def media_next_track(self):
""" media_next_track media player. """
pass
""" Send next track command. """
raise NotImplementedError()
def media_seek(self, position):
""" Send seek command. """
raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
pass
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
""" Boolean if pause is supported. """
return bool(self.supported_media_commands & SUPPORT_PAUSE)
@property
def support_seek(self):
""" Boolean if seek is supported. """
return bool(self.supported_media_commands & SUPPORT_SEEK)
@property
def support_volume_set(self):
""" Boolean if setting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_SET)
@property
def support_volume_mute(self):
""" Boolean if muting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE)
@property
def support_previous_track(self):
""" Boolean if previous track command supported. """
return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK)
@property
def support_next_track(self):
""" Boolean if next track command supported. """
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
@property
def support_youtube(self):
""" Boolean if YouTube is supported. """
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
def volume_up(self):
""" volume_up media player. """
if self.volume_level < 1:
self.set_volume_level(min(1, self.volume_level + .1))
def volume_down(self):
""" volume_down media player. """
if self.volume_level > 0:
self.set_volume_level(max(0, self.volume_level - .1))
def media_play_pause(self):
""" media_play_pause media player. """
if self.state == STATE_PLAYING:
self.media_pause()
else:
self.media_play()
@property
def state_attributes(self):
""" Return the state attributes. """
if self.state == STATE_OFF:
state_attr = {
ATTR_SUPPORTED_MEDIA_COMMANDS: self.supported_media_commands,
}
else:
state_attr = {
attr: getattr(self, attr) for attr
in ATTR_TO_PROPERTY if getattr(self, attr)
}
if self.media_image_url:
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
device_attr = self.device_state_attributes
if device_attr:
state_attr.update(device_attr)
return state_attr

View File

@ -14,18 +14,21 @@ try:
except ImportError:
pychromecast = None
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
STATE_UNKNOWN)
# ATTR_MEDIA_ALBUM, ATTR_MEDIA_IMAGE_URL,
# ATTR_MEDIA_ARTIST,
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, ATTR_MEDIA_TITLE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, ATTR_MEDIA_IS_VOLUME_MUTED,
MEDIA_STATE_PLAYING, MEDIA_STATE_PAUSED, MEDIA_STATE_STOPPED,
MEDIA_STATE_UNKNOWN)
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
# pylint: disable=unused-argument
@ -61,6 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CastDevice(MediaPlayerDevice):
""" Represents a Cast device on the network. """
# pylint: disable=too-many-public-methods
def __init__(self, host):
self.cast = pychromecast.Chromecast(host)
self.youtube = youtube.YouTubeController()
@ -73,6 +78,8 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status
# Entity properties and methods
@property
def should_poll(self):
return False
@ -82,57 +89,121 @@ class CastDevice(MediaPlayerDevice):
""" Returns the name of the device. """
return self.cast.device.friendly_name
# MediaPlayerDevice properties and methods
@property
def state(self):
""" Returns the state of the device. """
if self.cast.is_idle:
return STATE_NO_APP
""" State of the player. """
if self.media_status is None:
return STATE_UNKNOWN
elif self.media_status.player_is_playing:
return STATE_PLAYING
elif self.media_status.player_is_paused:
return STATE_PAUSED
elif self.media_status.player_is_idle:
return STATE_IDLE
elif self.cast.is_idle:
return STATE_OFF
else:
return self.cast.app_display_name
return STATE_UNKNOWN
@property
def media_state(self):
""" Returns the media state. """
media_controller = self.cast.media_controller
if media_controller.is_playing:
return MEDIA_STATE_PLAYING
elif media_controller.is_paused:
return MEDIA_STATE_PAUSED
elif media_controller.is_idle:
return MEDIA_STATE_STOPPED
else:
return MEDIA_STATE_UNKNOWN
def volume_level(self):
""" Volume level of the media player (0..1). """
return self.cast_status.volume_level if self.cast_status else None
@property
def state_attributes(self):
""" Returns the state attributes. """
cast_status = self.cast_status
media_status = self.media_status
media_controller = self.cast.media_controller
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self.cast_status.volume_muted if self.cast_status else None
state_attr = {
ATTR_MEDIA_STATE: self.media_state,
'application_id': self.cast.app_id,
}
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.media_status.content_id if self.media_status else None
if cast_status:
state_attr[ATTR_MEDIA_VOLUME] = cast_status.volume_level
state_attr[ATTR_MEDIA_IS_VOLUME_MUTED] = cast_status.volume_muted
@property
def media_content_type(self):
""" Content type of current playing media. """
if self.media_status is None:
return None
elif self.media_status.media_is_tvshow:
return MEDIA_TYPE_TVSHOW
elif self.media_status.media_is_movie:
return MEDIA_TYPE_VIDEO
elif self.media_status.media_is_musictrack:
return MEDIA_TYPE_MUSIC
return None
if media_status.content_id:
state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return self.media_status.duration if self.media_status else None
if media_status.duration:
state_attr[ATTR_MEDIA_DURATION] = media_status.duration
@property
def media_image_url(self):
""" Image url of current playing media. """
if self.media_status is None:
return None
if media_controller.title:
state_attr[ATTR_MEDIA_TITLE] = media_controller.title
images = self.media_status.images
if media_controller.thumbnail:
state_attr[ATTR_ENTITY_PICTURE] = media_controller.thumbnail
return images[0].url if images else None
return state_attr
@property
def media_title(self):
""" Title of current playing media. """
return self.media_status.title if self.media_status else None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.media_status.artist if self.media_status else None
@property
def media_album(self):
""" Album of current playing media. (Music track only) """
return self.media_status.album_name if self.media_status else None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return self.media_status.album_artist if self.media_status else None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self.media_status.track if self.media_status else None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return self.media_status.series_title if self.media_status else None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return self.media_status.season if self.media_status else None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self.media_status.episode if self.media_status else None
@property
def app_id(self):
""" ID of the current running app. """
return self.cast.app_id
@property
def app_name(self):
""" Name of the current running app. """
return self.cast.app_display_name
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_CAST
def turn_on(self):
""" Turns on the ChromeCast. """
@ -145,57 +216,42 @@ class CastDevice(MediaPlayerDevice):
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
def turn_off(self):
""" Service to exit any running app on the specimedia player ChromeCast and
shows idle screen. Will quit all ChromeCasts if nothing specified.
"""
""" Turns Chromecast off. """
self.cast.quit_app()
def volume_up(self):
""" Service to send the chromecast the command for volume up. """
self.cast.volume_up()
def volume_down(self):
""" Service to send the chromecast the command for volume down. """
self.cast.volume_down()
def volume_mute(self, mute):
""" Set media player to mute volume. """
def mute_volume(self, mute):
""" mute the volume. """
self.cast.set_volume_muted(mute)
def volume_set(self, volume):
""" Set media player volume, range of volume 0..1 """
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self.cast.set_volume(volume)
def media_play_pause(self):
""" Service to send the chromecast the command for play/pause. """
media_state = self.media_state
if media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
self.cast.media_controller.play()
elif media_state == MEDIA_STATE_PLAYING:
self.cast.media_controller.pause()
def media_play(self):
""" Service to send the chromecast the command for play/pause. """
if self.media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
self.cast.media_controller.play()
""" Send play commmand. """
self.cast.media_controller.play()
def media_pause(self):
""" Service to send the chromecast the command for play/pause. """
if self.media_state == MEDIA_STATE_PLAYING:
self.cast.media_controller.pause()
""" Send pause command. """
self.cast.media_controller.pause()
def media_prev_track(self):
""" media_prev_track media player. """
def media_previous_track(self):
""" Send previous track command. """
self.cast.media_controller.rewind()
def media_next_track(self):
""" media_next_track media player. """
""" Send next track command. """
self.cast.media_controller.skip()
def play_youtube_video(self, video_id):
""" Plays specified video_id on the Chromecast's YouTube channel. """
self.youtube.play_video(video_id)
def media_seek(self, position):
""" Seek the media to a specific location. """
self.cast.media_controller.seek(position)
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube.play_video(media_id)
# implementation of chromecast status_listener methods
def new_cast_status(self, status):
""" Called when a new cast status is received. """

View File

@ -5,121 +5,334 @@ homeassistant.components.media_player.demo
Demo implementation of the media player.
"""
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED,
YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED)
from homeassistant.const import ATTR_ENTITY_PICTURE
MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
add_devices([
DemoMediaPlayer(
DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'),
DemoMediaPlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours')
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'),
DemoMusicPlayer(), DemoTVShowPlayer(),
])
class DemoMediaPlayer(MediaPlayerDevice):
""" A Demo media player that only supports YouTube. """
YOUTUBE_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
def __init__(self, name, youtube_id=None, media_title=None):
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
class AbstractDemoPlayer(MediaPlayerDevice):
""" Base class for demo media players. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name):
self._name = name
self.is_playing = youtube_id is not None
self.youtube_id = youtube_id
self.media_title = media_title
self.volume = 1.0
self.is_volume_muted = False
self._player_state = STATE_PLAYING
self._volume_level = 1.0
self._volume_muted = False
@property
def should_poll(self):
""" No polling needed for a demo componentn. """
""" We will push an update after each command. """
return False
@property
def name(self):
""" Returns the name of the device. """
""" Name of the media player. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return STATE_NO_APP if self.youtube_id is None else "YouTube"
""" State of the player. """
return self._player_state
@property
def state_attributes(self):
""" Returns the state attributes. """
if self.youtube_id is None:
return
def volume_level(self):
""" Volume level of the media player (0..1). """
return self._volume_level
state_attr = {
ATTR_MEDIA_CONTENT_ID: self.youtube_id,
ATTR_MEDIA_TITLE: self.media_title,
ATTR_MEDIA_DURATION: 100,
ATTR_MEDIA_VOLUME: self.volume,
ATTR_MEDIA_IS_VOLUME_MUTED: self.is_volume_muted,
ATTR_ENTITY_PICTURE:
YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
}
if self.is_playing:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
else:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
return state_attr
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self._volume_muted
def turn_on(self):
""" turn_off media player. """
self.youtube_id = "eyU3bRy2x44"
self.is_playing = False
""" turn the media player on. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def turn_off(self):
""" turn_off media player. """
self.youtube_id = None
self.is_playing = False
""" turn the media player off. """
self._player_state = STATE_OFF
self.update_ha_state()
def volume_up(self):
""" volume_up media player. """
if self.volume < 1:
self.volume += 0.1
self.update_ha_state()
def volume_down(self):
""" volume_down media player. """
if self.volume > 0:
self.volume -= 0.1
self.update_ha_state()
def volume_mute(self, mute):
""" mute (true) or unmute (false) media player. """
self.is_volume_muted = mute
def mute_volume(self, mute):
""" mute the volume. """
self._volume_muted = mute
self.update_ha_state()
def media_play_pause(self):
""" media_play_pause media player. """
self.is_playing = not self.is_playing
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._volume_level = volume
self.update_ha_state()
def media_play(self):
""" media_play media player. """
self.is_playing = True
""" Send play commmand. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def media_pause(self):
""" media_pause media player. """
self.is_playing = False
""" Send pause command. """
self._player_state = STATE_PAUSED
self.update_ha_state()
class DemoYoutubePlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name, youtube_id=None, media_title=None):
super().__init__(name)
self.youtube_id = youtube_id
self._media_title = media_title
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.youtube_id
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_VIDEO
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 360
@property
def media_image_url(self):
""" Image url of current playing media. """
return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
@property
def media_title(self):
""" Title of current playing media. """
return self._media_title
@property
def app_name(self):
""" Current running app. """
return "YouTube"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return YOUTUBE_PLAYER_SUPPORT
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube_id = media_id
self.media_title = 'Demo media title'
self.is_playing = True
self._media_title = 'some YouTube video'
self.update_ha_state()
class DemoMusicPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
tracks = [
('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'),
('Paul Elstak', 'Luv U More'),
('Dune', 'Hardcore Vibes'),
('Nakatomi', 'Children Of The Night'),
('Party Animals',
'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'),
('Rob G.*', 'Ecstasy, You Got What I Need'),
('Lipstick', "I'm A Raver"),
('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'),
('Prophet', "The Big Boys Don't Cry"),
('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'),
('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'),
('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'),
('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'),
('Diss Reaction', 'Jiiieehaaaa '),
('Flamman And Abraxas', 'Good To Go (Radio Mix)'),
('Critical Mass', 'Dancing Together'),
('Charly Lownoise & Mental Theo',
'Ultimate Sex Track (Bass-D & King Matthew Remix)'),
]
def __init__(self):
super().__init__('Walkman')
self._cur_track = 0
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'bounzz-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 213
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/107771475912710/picture'
@property
def media_title(self):
""" Title of current playing media. """
return self.tracks[self._cur_track][1]
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.tracks[self._cur_track][0]
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
# pylint: disable=no-self-use
return "Bounzz"
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self._cur_track + 1
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = MUSIC_PLAYER_SUPPORT
if self._cur_track > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_track < len(self.tracks)-1:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_track > 0:
self._cur_track -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_track < len(self.tracks)-1:
self._cur_track += 1
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self):
super().__init__('Lounge room')
self._cur_episode = 1
self._episode_count = 13
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'house-of-cards-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_TVSHOW
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 3600
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/HouseofCards/picture'
@property
def media_title(self):
""" Title of current playing media. """
return 'Chapter {}'.format(self._cur_episode)
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return 'House of Cards'
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return 1
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self._cur_episode
@property
def app_name(self):
""" Current running app. """
return "Netflix"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = NETFLIX_PLAYER_SUPPORT
if self._cur_episode > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_episode < self._episode_count:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_episode > 1:
self._cur_episode -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_episode < self._episode_count:
self._cur_episode += 1
self.update_ha_state()

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

View File

@ -97,5 +97,5 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
# Tells the bootstrapper that the component was succesfully initialized
# Tells the bootstrapper that the component was successfully initialized
return True

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

View File

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

View File

@ -21,9 +21,26 @@ sensor:
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
- type: 'swap_use_percent'
- type: 'swap_use'
- type: 'swap_free'
- type: 'network_in'
arg: 'eth0'
- type: 'network_out'
arg: 'eth0'
- type: 'packets_in'
arg: 'eth0'
- type: 'packets_out'
arg: 'eth0'
- type: 'ipv4_address'
arg: 'eth0'
- type: 'ipv6_address'
arg: 'eth0'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
- type: 'last_boot'
- type: 'since_last_boot'
Variables:
@ -42,12 +59,12 @@ arg
*Optional
Additional details for the type, eg. path, binary name, etc.
"""
import logging
import psutil
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.const import STATE_ON, STATE_OFF
import psutil
import logging
SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'],
@ -58,6 +75,17 @@ SENSOR_TYPES = {
'memory_free': ['RAM Free', 'MiB'],
'processor_use': ['CPU Use', '%'],
'process': ['Process', ''],
'swap_use_percent': ['Swap Use', '%'],
'swap_use': ['Swap Use', 'GiB'],
'swap_free': ['Swap Free', 'GiB'],
'network_out': ['Sent', 'MiB'],
'network_in': ['Recieved', 'MiB'],
'packets_out': ['Packets sent', ''],
'packets_in': ['Packets recieved', ''],
'ipv4_address': ['IPv4 address', ''],
'ipv6_address': ['IPv6 address', ''],
'last_boot': ['Last Boot', ''],
'since_last_boot': ['Since Last Boot', '']
}
_LOGGER = logging.getLogger(__name__)
@ -103,6 +131,7 @@ class SystemMonitorSensor(Entity):
def unit_of_measurement(self):
return self._unit_of_measurement
# pylint: disable=too-many-branches
def update(self):
if self.type == 'disk_use_percent':
self._state = psutil.disk_usage(self.argument).percent
@ -120,6 +149,12 @@ class SystemMonitorSensor(Entity):
1024**2, 1)
elif self.type == 'memory_free':
self._state = round(psutil.virtual_memory().available / 1024**2, 1)
elif self.type == 'swap_use_percent':
self._state = psutil.swap_memory().percent
elif self.type == 'swap_use':
self._state = round(psutil.swap_memory().used / 1024**3, 1)
elif self.type == 'swap_free':
self._state = round(psutil.swap_memory().free / 1024**3, 1)
elif self.type == 'processor_use':
self._state = round(psutil.cpu_percent(interval=None))
elif self.type == 'process':
@ -127,3 +162,24 @@ class SystemMonitorSensor(Entity):
self._state = STATE_ON
else:
self._state = STATE_OFF
elif self.type == 'network_out':
self._state = round(psutil.net_io_counters(pernic=True)
[self.argument][0] / 1024**2, 1)
elif self.type == 'network_in':
self._state = round(psutil.net_io_counters(pernic=True)
[self.argument][1] / 1024**2, 1)
elif self.type == 'packets_out':
self._state = psutil.net_io_counters(pernic=True)[self.argument][2]
elif self.type == 'packets_in':
self._state = psutil.net_io_counters(pernic=True)[self.argument][3]
elif self.type == 'ipv4_address':
self._state = psutil.net_if_addrs()[self.argument][0][1]
elif self.type == 'ipv6_address':
self._state = psutil.net_if_addrs()[self.argument][1][1]
elif self.type == 'last_boot':
self._state = dt_util.datetime_to_date_str(
dt_util.as_local(
dt_util.utc_from_timestamp(psutil.boot_time())))
elif self.type == 'since_last_boot':
self._state = dt_util.utcnow() - dt_util.utc_from_timestamp(
psutil.boot_time())

View File

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

View File

@ -7,6 +7,7 @@ import logging
from datetime import timedelta
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -33,6 +34,11 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SWITCHES: 'isy994',
}
PROP_TO_ATTR = {
'current_power_mwh': ATTR_CURRENT_POWER_MWH,
'today_power_mw': ATTR_TODAY_MWH,
}
_LOGGER = logging.getLogger(__name__)
@ -74,10 +80,48 @@ def setup(hass, config):
else:
switch.turn_off()
switch.update_ha_state(True)
if switch.should_poll:
switch.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
return True
class SwitchDevice(ToggleEntity):
""" Represents a switch within Home Assistant. """
# pylint: disable=no-self-use
@property
def current_power_mwh(self):
""" Current power usage in mwh. """
return None
@property
def today_power_mw(self):
""" Today total power usage in mw. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

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

View File

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

View File

@ -3,6 +3,12 @@ homeassistant.components.switch.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Tellstick switches.
Because the tellstick sends its actions via radio and from most
receivers it's impossible to know if the signal was received or not.
Therefore you can configure the switch to try to send each signal repeatedly
with the config parameter signal_repetitions (default is 1).
signal_repetitions: 3
"""
import logging
@ -11,6 +17,8 @@ from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
SINGAL_REPETITIONS = 1
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -22,6 +30,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
core = telldus.TelldusCore()
switches_and_lights = core.devices()
@ -29,7 +39,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
for switch in switches_and_lights:
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
switches.append(TellstickSwitchDevice(switch))
switches.append(TellstickSwitchDevice(switch, signal_repetitions))
add_devices_callback(switches)
@ -39,9 +49,10 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)
def __init__(self, tellstick):
def __init__(self, tellstick, signal_repetitions):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self.signal_repetitions = signal_repetitions
@property
def name(self):
@ -63,8 +74,10 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.tellstick.turn_on()
for _ in range(self.signal_repetitions):
self.tellstick.turn_on()
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
for _ in range(self.signal_repetitions):
self.tellstick.turn_off()

View File

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

View File

@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
def update(self):
""" 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_OPEN = 'open'
STATE_CLOSED = 'closed'
STATE_PLAYING = 'playing'
STATE_PAUSED = 'paused'
STATE_IDLE = 'idle'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
@ -104,7 +107,8 @@ SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
SERVICE_MEDIA_PLAY = "media_play"
SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
SERVICE_MEDIA_SEEK = "media_seek"
# #### API / REMOTE ####
SERVER_PORT = 8123

View File

@ -9,9 +9,9 @@ import datetime as dt
import pytz
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_SHORT_STR_FORMAT = "%Y-%m-%d"
TIME_SHORT_STR_FORMAT = "%H:%M"
DATETIME_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_STR_FORMAT = "%Y-%m-%d"
TIME_STR_FORMAT = "%H:%M"
UTC = DEFAULT_TIME_ZONE = pytz.utc
@ -34,7 +34,7 @@ def get_time_zone(time_zone_str):
def utcnow():
""" Get now in UTC time. """
return dt.datetime.now(pytz.utc)
return dt.datetime.now(UTC)
def now(time_zone=None):
@ -45,12 +45,12 @@ def now(time_zone=None):
def as_utc(dattim):
""" Return a datetime as UTC time.
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """
if dattim.tzinfo == pytz.utc:
if dattim.tzinfo == UTC:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
return dattim.astimezone(pytz.utc)
return dattim.astimezone(UTC)
def as_local(dattim):
@ -58,17 +58,28 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=pytz.utc)
dattim = dattim.replace(tzinfo=UTC)
return dattim.astimezone(DEFAULT_TIME_ZONE)
def utc_from_timestamp(timestamp):
""" Returns a UTC time from a timestamp. """
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc)
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC)
def datetime_to_local_str(dattim, time_zone=None):
def start_of_local_day(dt_or_d=None):
""" Return local datetime object of start of day from date or datetime. """
if dt_or_d is None:
dt_or_d = now().date()
elif isinstance(dt_or_d, dt.datetime):
dt_or_d = dt_or_d.date()
return dt.datetime.combine(dt_or_d, dt.time()).replace(
tzinfo=DEFAULT_TIME_ZONE)
def datetime_to_local_str(dattim):
""" Converts datetime to specified time_zone and returns a string. """
return datetime_to_str(as_local(dattim))
@ -76,27 +87,27 @@ def datetime_to_local_str(dattim, time_zone=None):
def datetime_to_str(dattim):
""" Converts datetime to a string format.
@rtype : str
"""
return dattim.strftime(DATETIME_STR_FORMAT)
def datetime_to_time_str(dattim):
""" Converts datetime to a string containing only the time.
@rtype : str
"""
return dattim.strftime(TIME_STR_FORMAT)
def datetime_to_date_str(dattim):
""" Converts datetime to a string containing only the date.
@rtype : str
"""
return dattim.strftime(DATE_STR_FORMAT)
def datetime_to_short_time_str(dattim):
""" Converts datetime to a string format as short time.
@rtype : str
"""
return dattim.strftime(TIME_SHORT_STR_FORMAT)
def datetime_to_short_date_str(dattim):
""" Converts datetime to a string format as short date.
@rtype : str
"""
return dattim.strftime(DATE_SHORT_STR_FORMAT)
def str_to_datetime(dt_str):
""" Converts a string to a UTC datetime object.
@ -104,7 +115,15 @@ def str_to_datetime(dt_str):
"""
try:
return dt.datetime.strptime(
dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc)
dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc)
except ValueError: # If dt_str did not match our format
return None
def date_str_to_date(dt_str):
""" Converts a date string to a date object. """
try:
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
except ValueError: # If dt_str did not match our format
return None

View File

@ -18,7 +18,7 @@ phue>=0.8
ledcontroller>=1.0.7
# Chromecast bindings (media_player.cast)
pychromecast>=0.6.4
pychromecast>=0.6.6
# Keyboard (keyboard)
pyuserinput>=0.1.9
@ -39,10 +39,10 @@ python-nest>=2.3.1
pydispatcher>=2.0.5
# ISY994 bindings (*.isy994)
PyISY>=1.0.2
PyISY>=1.0.5
# PSutil (sensor.systemmonitor)
psutil>=2.2.1
psutil>=3.0.0
# Pushover bindings (notify.pushover)
python-pushover>=0.2
@ -51,7 +51,7 @@ python-pushover>=0.2
transmissionrpc>=0.11
# OpenWeatherMap Web API (sensor.openweathermap)
pyowm>=2.2.0
pyowm>=2.2.1
# XMPP Bindings (notify.xmpp)
sleekxmpp>=1.3.1
@ -64,3 +64,18 @@ python-mpd2>=0.5.4
# Hikvision (switch.hikvisioncam)
hikvision>=0.4
# console log coloring
colorlog>=2.6.0
# JSON-RPC interface
jsonrpc-requests>=0.1
# Forecast.io Bindings (sensor.forecast)
python-forecastio>=1.3.3
# Firmata Bindings (*.arduino)
PyMata==2.07a
# Mysensors serial gateway
pyserial>=2.7

View File

@ -10,9 +10,10 @@ import unittest
import homeassistant as ha
from homeassistant.const import (
STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID)
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID)
import homeassistant.components.media_player as media_player
from helpers import mock_service
@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase):
self.hass = ha.HomeAssistant()
self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room')
self.hass.states.set(self.test_entity, media_player.STATE_NO_APP)
self.hass.states.set(self.test_entity, STATE_OFF)
self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom')
self.hass.states.set(self.test_entity2, "YouTube")
@ -56,7 +57,7 @@ class TestMediaPlayer(unittest.TestCase):
SERVICE_MEDIA_PLAY: media_player.media_play,
SERVICE_MEDIA_PAUSE: media_player.media_pause,
SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track,
SERVICE_MEDIA_PREV_TRACK: media_player.media_prev_track
SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track
}
for service_name, service_method in services.items():