mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Merged in upstream changes
This commit is contained in:
commit
3440c54ab7
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master)
|
||||
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master) [](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
This is the source code for Home Assistant. For installation instructions, tutorials and the docs, please see [the website](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
|
||||
|
||||
@ -8,7 +8,7 @@ It offers the following functionality through built-in components:
|
||||
|
||||
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index))
|
||||
* Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
|
||||
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/)
|
||||
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and XBMC/Kodi (http://kodi.tv/)
|
||||
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/)
|
||||
* Track running system services and monitoring your system stats (Memory, disk usage, and more)
|
||||
* Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands
|
||||
|
@ -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
|
||||
|
@ -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__)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
134
homeassistant/components/arduino.py
Normal file
134
homeassistant/components/arduino.py
Normal 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()
|
@ -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"
|
||||
]
|
||||
})
|
||||
|
||||
|
117
homeassistant/components/device_tracker/tplink.py
Executable file
117
homeassistant/components/device_tracker/tplink.py
Executable 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
|
@ -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(
|
||||
|
@ -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
@ -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",
|
||||
|
@ -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 '';
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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});
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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});
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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});
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../resources/lodash.html">
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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];
|
||||
},
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -80,7 +80,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
eventActions.fire(this.eventType, eventData);
|
||||
eventActions.fireEvent(this.eventType, eventData);
|
||||
},
|
||||
|
||||
computeFormClasses: function(narrow) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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;
|
||||
|
||||
|
@ -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 ?
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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>
|
@ -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) {
|
||||
|
@ -0,0 +1,2 @@
|
||||
<script src="../bower_components/pikaday/pikaday.js"></script>
|
||||
<link href="../bower_components/pikaday/css/pikaday.css" media="all" rel="stylesheet" />
|
@ -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
@ -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())
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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. """
|
||||
|
@ -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()
|
||||
|
308
homeassistant/components/media_player/kodi.py
Normal file
308
homeassistant/components/media_player/kodi.py
Normal 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()
|
@ -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()
|
||||
|
@ -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
|
||||
|
78
homeassistant/components/notify/file.py
Normal file
78
homeassistant/components/notify/file.py
Normal 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)
|
110
homeassistant/components/notify/syslog.py
Normal file
110
homeassistant/components/notify/syslog.py
Normal 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()
|
87
homeassistant/components/sensor/arduino.py
Normal file
87
homeassistant/components/sensor/arduino.py
Normal 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]
|
@ -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)
|
||||
|
216
homeassistant/components/sensor/forecast.py
Normal file
216
homeassistant/components/sensor/forecast.py
Normal 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()
|
@ -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()
|
||||
|
@ -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']))
|
||||
)
|
||||
|
@ -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())
|
||||
|
@ -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(':')
|
||||
|
@ -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
|
||||
|
93
homeassistant/components/switch/arduino.py
Normal file
93
homeassistant/components/switch/arduino.py
Normal 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)
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
|
||||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
self.wink.wait_till_desired_reached()
|
||||
self.wink.updateState()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
Loading…
x
Reference in New Issue
Block a user