mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
ES6 Polymer version
This commit is contained in:
commit
8e143c2e44
11
.eslintrc
Normal file
11
.eslintrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "eslint-config-airbnb",
|
||||
"globals": {
|
||||
"__DEV__": false,
|
||||
"__DEMO__": false
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": [2, "always-multiline"],
|
||||
"no-underscore-dangle": false
|
||||
}
|
||||
}
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/*
|
||||
node_modules/*
|
||||
bower_components/*
|
44
bower.json
Normal file
44
bower.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"version": "0.1.0",
|
||||
"authors": [
|
||||
"Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
],
|
||||
"main": "splash-login.html",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
],
|
||||
"devDependencies": {
|
||||
"polymer": "Polymer/polymer#^1.0.0",
|
||||
"webcomponentsjs": "Polymer/webcomponentsjs#^0.7",
|
||||
"paper-header-panel": "PolymerElements/paper-header-panel#^1.0.0",
|
||||
"paper-toolbar": "PolymerElements/paper-toolbar#^1.0.0",
|
||||
"paper-menu": "PolymerElements/paper-menu#^1.0.0",
|
||||
"iron-input": "PolymerElements/iron-input#^1.0.0",
|
||||
"iron-icons": "PolymerElements/iron-icons#^1.0.0",
|
||||
"iron-image": "PolymerElements/iron-image#^1.0.0",
|
||||
"paper-toast": "PolymerElements/paper-toast#^1.0.0",
|
||||
"paper-dialog": "PolymerElements/paper-dialog#^1.0.0",
|
||||
"paper-dialog-scrollable": "polymerelements/paper-dialog-scrollable#^1.0.0",
|
||||
"paper-spinner": "PolymerElements/paper-spinner#^1.0.0",
|
||||
"paper-button": "PolymerElements/paper-button#^1.0.0",
|
||||
"paper-input": "PolymerElements/paper-input#^1.0.0",
|
||||
"paper-toggle-button": "PolymerElements/paper-toggle-button#^1.0.0",
|
||||
"paper-icon-button": "PolymerElements/paper-icon-button#^1.0.0",
|
||||
"paper-item": "PolymerElements/paper-item#^1.0.0",
|
||||
"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.0",
|
||||
"google-apis": "GoogleWebComponents/google-apis#0.8-preview",
|
||||
"layout": "Polymer/layout",
|
||||
"paper-styles": "polymerelements/paper-styles#^1.0.0",
|
||||
"pikaday": "~1.3.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"polymer": "^1.0.0",
|
||||
"webcomponentsjs": "^0.7.0"
|
||||
}
|
||||
}
|
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "home-assistant-polymer",
|
||||
"version": "1.0.0",
|
||||
"description": "A frontend for Home Assistant using the Polymer framework",
|
||||
"scripts": {
|
||||
"js_dev": "webpack --colors --progress -d --watch",
|
||||
"js_dev_demo": "BUILD_DEMO=1 webpack --colors --progress -d --watch",
|
||||
"js_prod": "BUILD_DEV=0 webpack --colors --progress -p -d",
|
||||
"js_demo": "BUILD_DEV=0 BUILD_DEMO=1 webpack --colors --progress -p -d",
|
||||
"frontend_html": "vulcanize --inline-css --inline-scripts --strip-comments src/home-assistant.html > build/frontend.vulcan.html",
|
||||
"frontend_minify": "node scripts/minify.js",
|
||||
"frontend_prod": "npm run js_prod && bower install && npm run frontend_html && npm run frontend_minify",
|
||||
"frontend_demo": "npm run js_demo && bower install && npm run frontend_html && npm run frontend_minify"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"babel-core": "^5.6.18",
|
||||
"babel-loader": "^5.3.1",
|
||||
"bower": "^1.4.1",
|
||||
"eslint-config-airbnb": "0.0.6",
|
||||
"home-assistant-js": "git+https://github.com/balloob/home-assistant-js.git#d6159b2654e070e13017bc8f99418246c5112d9a",
|
||||
"html-minifier": "^0.7.2",
|
||||
"lodash": "^3.10.0",
|
||||
"node-libs-browser": "^0.5.2",
|
||||
"vulcanize": "^1.10.1",
|
||||
"webpack": "^1.10.1"
|
||||
}
|
||||
}
|
20
scripts/minify.js
Executable file
20
scripts/minify.js
Executable file
@ -0,0 +1,20 @@
|
||||
var minify = require('html-minifier');
|
||||
var fs = require('fs');
|
||||
|
||||
var html = fs.readFileSync('build/frontend.vulcan.html').toString();
|
||||
|
||||
// removeComments: true,
|
||||
// collapseWhitespace: true,
|
||||
var minifiedHtml = minify.minify(html, {
|
||||
customAttrAssign: [/\$=/],
|
||||
"removeComments": true,
|
||||
"removeCommentsFromCDATA": true,
|
||||
"removeCDATASectionsFromCDATA": true,
|
||||
"collapseWhitespace": true,
|
||||
"collapseBooleanAttributes": true,
|
||||
"removeScriptTypeAttributes": true,
|
||||
"removeStyleLinkTypeAttributes": true,
|
||||
"minifyJS": true,
|
||||
});
|
||||
|
||||
fs.writeFileSync('build/frontend.html', minifiedHtml);
|
15
src/cards/state-card-configurator.html
Normal file
15
src/cards/state-card-configurator.html
Normal file
@ -0,0 +1,15 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel='import' href='./state-card-display.html'>
|
||||
<link rel='import' href='../components/state-info.html'>
|
||||
|
||||
<dom-module id='state-card-configurator'>
|
||||
<template>
|
||||
<state-card-display state-obj='[[stateObj]]'></state-card-display>
|
||||
|
||||
<!-- pre load the image so the dialog is rendered the proper size -->
|
||||
<template is='dom-if' if='[[stateObj.attributes.description_image]]'>
|
||||
<img hidden src='[[stateObj.attributes.description_image]]' />
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
14
src/cards/state-card-configurator.js
Normal file
14
src/cards/state-card-configurator.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('../components/state-info');
|
||||
require('./state-card-display');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-configurator',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
8
src/cards/state-card-content.html
Normal file
8
src/cards/state-card-content.html
Normal file
@ -0,0 +1,8 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-card-display.html">
|
||||
<link rel="import" href="state-card-toggle.html">
|
||||
<link rel="import" href="state-card-thermostat.html">
|
||||
<link rel="import" href="state-card-configurator.html">
|
||||
<link rel="import" href="state-card-scene.html">
|
||||
<link rel="import" href="state-card-media_player.html">
|
46
src/cards/state-card-content.js
Normal file
46
src/cards/state-card-content.js
Normal file
@ -0,0 +1,46 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
import stateCardType from '../util/state-card-type';
|
||||
|
||||
require('./state-card-display');
|
||||
require('./state-card-toggle');
|
||||
require('./state-card-thermostat');
|
||||
require('./state-card-configurator');
|
||||
require('./state-card-scene');
|
||||
require('./state-card-media_player');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-content',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (!newVal) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var newCardType = stateCardType(newVal);
|
||||
|
||||
if (!oldVal || stateCardType(oldVal) != newCardType) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + newCardType);
|
||||
stateCard.stateObj = newVal;
|
||||
root.appendChild(stateCard);
|
||||
} else {
|
||||
root.lastChild.stateObj = newVal;
|
||||
}
|
||||
},
|
||||
});
|
22
src/cards/state-card-display.html
Executable file
22
src/cards/state-card-display.html
Executable file
@ -0,0 +1,22 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<dom-module id="state-card-display">
|
||||
<style>
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
<div class='state'>[[stateObj.stateDisplay]]</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
13
src/cards/state-card-display.js
Normal file
13
src/cards/state-card-display.js
Normal file
@ -0,0 +1,13 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('../components/state-info');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-display',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
41
src/cards/state-card-media_player.html
Normal file
41
src/cards/state-card-media_player.html
Normal file
@ -0,0 +1,41 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<dom-module id="state-card-media_player">
|
||||
<style>
|
||||
:host {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
<div class='state'>
|
||||
<div class='main-text'>[[computePrimaryText(stateObj, isPlaying)]]</div>
|
||||
<div class='secondary-text'>[[computeSecondaryText(stateObj, isPlaying)]]</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
50
src/cards/state-card-media_player.js
Normal file
50
src/cards/state-card-media_player.js
Normal file
@ -0,0 +1,50 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('../components/state-info');
|
||||
|
||||
const PLAYING_STATES = ['playing', 'paused'];
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-media_player',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsPlaying(stateObj)',
|
||||
},
|
||||
},
|
||||
|
||||
computeIsPlaying: function(stateObj) {
|
||||
return PLAYING_STATES.indexOf(stateObj.state) !== -1;
|
||||
},
|
||||
|
||||
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 '';
|
||||
}
|
||||
},
|
||||
});
|
15
src/cards/state-card-scene.html
Normal file
15
src/cards/state-card-scene.html
Normal file
@ -0,0 +1,15 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="./state-card-display.html">
|
||||
<link rel="import" href="./state-card-toggle.html">
|
||||
|
||||
<dom-module id="state-card-scene">
|
||||
<template>
|
||||
<template is='dom-if' if='[[allowToggle]]'>
|
||||
<state-card-toggle state-obj="[[stateObj]]"></state-card-toggle>
|
||||
</template>
|
||||
<template is='dom-if' if='[[!allowToggle]]'>
|
||||
<state-card-display state-obj="[[stateObj]]"></state-card-display>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
24
src/cards/state-card-scene.js
Normal file
24
src/cards/state-card-scene.js
Normal file
@ -0,0 +1,24 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./state-card-display');
|
||||
require('./state-card-toggle');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-scene',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
allowToggle: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
computed: 'computeAllowToggle(stateObj)',
|
||||
},
|
||||
},
|
||||
|
||||
computeAllowToggle: function(stateObj) {
|
||||
return stateObj.state === 'off' || stateObj.attributes.active_requested;
|
||||
},
|
||||
});
|
43
src/cards/state-card-thermostat.html
Normal file
43
src/cards/state-card-thermostat.html
Normal file
@ -0,0 +1,43 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<dom-module id="state-card-thermostat">
|
||||
<style>
|
||||
:host {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.target {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.current {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
<div class='state'>
|
||||
<div class='target'>[[stateObj.stateDisplay]]</div>
|
||||
|
||||
<div class='current'>
|
||||
<span>Currently: </span>
|
||||
<span>[[stateObj.attributes.current_temperature]]</span>
|
||||
<span> </span>
|
||||
<span>[[stateObj.attributes.unit_of_measurement]]</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
13
src/cards/state-card-thermostat.js
Normal file
13
src/cards/state-card-thermostat.js
Normal file
@ -0,0 +1,13 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('../components/state-info');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-thermostat',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
24
src/cards/state-card-toggle.html
Executable file
24
src/cards/state-card-toggle.html
Executable file
@ -0,0 +1,24 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<dom-module id="state-card-toggle">
|
||||
<style>
|
||||
paper-toggle-button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
|
||||
<paper-toggle-button class='self-center'
|
||||
checked="[[toggleChecked]]"
|
||||
on-change="toggleChanged"
|
||||
on-tap="toggleTapped">
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
70
src/cards/state-card-toggle.js
Normal file
70
src/cards/state-card-toggle.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { serviceActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('../components/state-info');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card-toggle',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
toggleChecked: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
ready() {
|
||||
this.forceStateChange = this.forceStateChange.bind(this);
|
||||
this.forceStateChange();
|
||||
},
|
||||
|
||||
toggleTapped(ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
toggleChanged(ev) {
|
||||
var newVal = ev.target.checked;
|
||||
|
||||
if(newVal && this.stateObj.state === "off") {
|
||||
this.turn_on();
|
||||
} else if(!newVal && this.stateObj.state === "on") {
|
||||
this.turn_off();
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged(newVal) {
|
||||
if (newVal) {
|
||||
this.updateToggle(newVal);
|
||||
}
|
||||
},
|
||||
|
||||
updateToggle(stateObj) {
|
||||
this.toggleChecked = stateObj && stateObj.state === "on";
|
||||
},
|
||||
|
||||
forceStateChange() {
|
||||
this.updateToggle(this.stateObj);
|
||||
},
|
||||
|
||||
turn_on() {
|
||||
// We call updateToggle after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
serviceActions.callTurnOn(this.stateObj.entityId).then(this.forceStateChange);
|
||||
},
|
||||
|
||||
turn_off() {
|
||||
// We call updateToggle after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
serviceActions.callTurnOff(this.stateObj.entityId).then(this.forceStateChange);
|
||||
},
|
||||
});
|
29
src/cards/state-card.html
Normal file
29
src/cards/state-card.html
Normal file
@ -0,0 +1,29 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-card-content.html">
|
||||
|
||||
<dom-module id="state-card">
|
||||
<style>
|
||||
:host {
|
||||
border-radius: 2px;
|
||||
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
|
||||
transition: all 0.30s ease-out;
|
||||
|
||||
position: relative;
|
||||
background-color: white;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<state-card-content state-obj="[[stateObj]]"></state-card-content>
|
||||
</template>
|
||||
</dom-module>
|
25
src/cards/state-card.js
Normal file
25
src/cards/state-card.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { moreInfoActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./state-card-content');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-card',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'tap': 'cardTapped',
|
||||
},
|
||||
|
||||
cardTapped(ev) {
|
||||
ev.stopPropagation();
|
||||
this.async(moreInfoActions.selectEntity.bind(
|
||||
this, this.stateObj.entityId), 100);
|
||||
},
|
||||
});
|
5
src/components/display-time.html
Normal file
5
src/components/display-time.html
Normal file
@ -0,0 +1,5 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id="display-time">
|
||||
<template>[[computeTime(dateObj)]]</template>
|
||||
</dom-module>
|
17
src/components/display-time.js
Normal file
17
src/components/display-time.js
Normal file
@ -0,0 +1,17 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
import formatTime from '../util/format-time';
|
||||
|
||||
export default Polymer({
|
||||
is: 'display-time',
|
||||
|
||||
properties: {
|
||||
dateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
computeTime: function(dateObj) {
|
||||
return dateObj ? formatTime(dateObj) : '';
|
||||
},
|
||||
});
|
11
src/components/domain-icon.html
Normal file
11
src/components/domain-icon.html
Normal file
@ -0,0 +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="../resources/home-assistant-icons.html">
|
||||
|
||||
<dom-module id="domain-icon">
|
||||
<template>
|
||||
<iron-icon icon="[[computeIcon(domain, state)]]"></iron-icon>
|
||||
</template>
|
||||
</dom-module>
|
23
src/components/domain-icon.js
Normal file
23
src/components/domain-icon.js
Normal file
@ -0,0 +1,23 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
import domainIcon from '../util/domain-icon';
|
||||
|
||||
export default Polymer({
|
||||
is: 'domain-icon',
|
||||
|
||||
properties: {
|
||||
domain: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
state: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
computeIcon(domain, state) {
|
||||
return domainIcon(domain, state);
|
||||
},
|
||||
});
|
27
src/components/entity-list.html
Normal file
27
src/components/entity-list.html
Normal file
@ -0,0 +1,27 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id="entity-list">
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<template is='dom-repeat' items='[[entities]]' as='entity'>
|
||||
<li><a href='#' on-click='entitySelected'>[[entity.entityId]]</a></li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
</dom-module>
|
29
src/components/entity-list.js
Normal file
29
src/components/entity-list.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { entityGetters } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
export default Polymer({
|
||||
is: 'entity-list',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
entities: {
|
||||
type: Array,
|
||||
bindNuclear: [
|
||||
entityGetters.entityMap,
|
||||
function(map) {
|
||||
return map.valueSeq().
|
||||
sortBy(function(entity) { return entity.entityId; })
|
||||
.toArray();
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
entitySelected: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.fire('entity-selected', {entityId: ev.model.entity.entityId});
|
||||
},
|
||||
});
|
30
src/components/events-list.html
Normal file
30
src/components/events-list.html
Normal file
@ -0,0 +1,30 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id="events-list">
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<template is='dom-repeat' items='[[events]]' as='event'>
|
||||
<li>
|
||||
<a href='#' on-click='eventSelected'>{{event.event}}</a>
|
||||
<span> (</span><span>{{event.listener_count}}</span><span> listeners)</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
</dom-module>
|
29
src/components/events-list.js
Normal file
29
src/components/events-list.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { eventGetters } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
export default Polymer({
|
||||
is: 'events-list',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
events: {
|
||||
type: Array,
|
||||
bindNuclear: [
|
||||
eventGetters.entityMap,
|
||||
function(map) {
|
||||
return map.valueSeq()
|
||||
.sortBy(function(event) { return event.event; })
|
||||
.toArray();
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
eventSelected(ev) {
|
||||
ev.preventDefault();
|
||||
this.fire('event-selected', {eventType: ev.model.event.event});
|
||||
},
|
||||
});
|
12
src/components/ha-color-picker.html
Normal file
12
src/components/ha-color-picker.html
Normal file
@ -0,0 +1,12 @@
|
||||
<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>
|
151
src/components/ha-color-picker.js
Normal file
151
src/components/ha-color-picker.js
Normal file
@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Color-picker custom element
|
||||
* Originally created by bbrewer97202 (Ben Brewer). MIT Licensed.
|
||||
* https://github.com/bbrewer97202/color-picker-element
|
||||
*
|
||||
* Adapted to work with Polymer.
|
||||
*/
|
||||
import Polymer from '../polymer';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
export default 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(e) {
|
||||
this.onMouseMove(e);
|
||||
this.addEventListener('mousemove', this.onMouseMove);
|
||||
},
|
||||
|
||||
onMouseUp(e) {
|
||||
this.removeEventListener('mousemove', this.onMouseMove);
|
||||
},
|
||||
|
||||
onTouchStart(e) {
|
||||
this.onTouchMove(e);
|
||||
this.addEventListener('touchmove', this.onTouchMove);
|
||||
},
|
||||
|
||||
onTouchEnd(e) {
|
||||
this.removeEventListener('touchmove', this.onTouchMove);
|
||||
},
|
||||
|
||||
onTap(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
onTouchMove(e) {
|
||||
var touch = e.touches[0];
|
||||
this.onColorSelect(e, {x: touch.clientX, y: touch.clientY});
|
||||
},
|
||||
|
||||
onMouseMove(e) {
|
||||
e.preventDefault();
|
||||
if (this.mouseMoveIsThrottled) {
|
||||
this.mouseMoveIsThrottled = false;
|
||||
this.onColorSelect(e);
|
||||
this.async(
|
||||
function() { this.mouseMoveIsThrottled = true; }.bind(this), 100);
|
||||
}
|
||||
},
|
||||
|
||||
onColorSelect(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(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(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() {
|
||||
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);
|
||||
},
|
||||
|
||||
});
|
20
src/components/ha-logbook.html
Normal file
20
src/components/ha-logbook.html
Normal file
@ -0,0 +1,20 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/logbook-entry.html">
|
||||
|
||||
<dom-module id="ha-logbook">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
</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>
|
||||
</template>
|
||||
</dom-module>
|
16
src/components/ha-logbook.js
Normal file
16
src/components/ha-logbook.js
Normal file
@ -0,0 +1,16 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
Polymer({
|
||||
is: 'ha-logbook',
|
||||
|
||||
properties: {
|
||||
entries: {
|
||||
type: Object,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
noEntries: function(entries) {
|
||||
return !entries.length;
|
||||
}
|
||||
});
|
128
src/components/ha-sidebar.html
Normal file
128
src/components/ha-sidebar.html
Normal file
@ -0,0 +1,128 @@
|
||||
<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>
|
121
src/components/ha-sidebar.js
Normal file
121
src/components/ha-sidebar.js
Normal file
@ -0,0 +1,121 @@
|
||||
import {
|
||||
configGetters,
|
||||
navigationGetters,
|
||||
authActions,
|
||||
navigationActions,
|
||||
util
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
import domainIcon from '../util/domain-icon';
|
||||
|
||||
require('./stream-status');
|
||||
|
||||
const { entityDomainFilters } = util;
|
||||
|
||||
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(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(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(ev) {
|
||||
// prevent it from highlighting first menu item
|
||||
document.activeElement.blur();
|
||||
this.menuClicked(ev);
|
||||
},
|
||||
|
||||
selectPanel(newChoice) {
|
||||
if(newChoice === this.selected) {
|
||||
return;
|
||||
} else if (newChoice == 'logout') {
|
||||
this.handleLogOut();
|
||||
return;
|
||||
}
|
||||
navigationActions.navigate.apply(null, newChoice.split('/'));
|
||||
},
|
||||
|
||||
handleLogOut() {
|
||||
authActions.logOut();
|
||||
},
|
||||
|
||||
filterIcon(filter) {
|
||||
return domainIcon(filter);
|
||||
},
|
||||
|
||||
filterName(filter) {
|
||||
return entityDomainFilters[filter];
|
||||
},
|
||||
|
||||
filterType(filter) {
|
||||
return 'states/' + filter;
|
||||
},
|
||||
});
|
32
src/components/ha-voice-command-progress.html
Normal file
32
src/components/ha-voice-command-progress.html
Normal file
@ -0,0 +1,32 @@
|
||||
<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>
|
27
src/components/ha-voice-command-progress.js
Normal file
27
src/components/ha-voice-command-progress.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { voiceGetters } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
export default 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,
|
||||
},
|
||||
},
|
||||
});
|
19
src/components/loading-box.html
Normal file
19
src/components/loading-box.html
Normal file
@ -0,0 +1,19 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<dom-module id="loading-box">
|
||||
<style>
|
||||
.text {
|
||||
display: inline-block;
|
||||
line-height: 28px;
|
||||
vertical-align: top;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div layout='horizontal'>
|
||||
<paper-spinner active="true"></paper-spinner>
|
||||
<div class='text'><content></content>…</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
5
src/components/loading-box.js
Normal file
5
src/components/loading-box.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'loading-box',
|
||||
});
|
53
src/components/logbook-entry.html
Normal file
53
src/components/logbook-entry.html
Normal file
@ -0,0 +1,53 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
<link rel="import" href="display-time.html">
|
||||
<link rel="import" href="relative-ha-datetime.html">
|
||||
|
||||
<dom-module id="logbook-entry">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
display-time {
|
||||
width: 55px;
|
||||
font-size: .8em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
domain-icon {
|
||||
margin: 0 8px 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='horizontal layout'>
|
||||
<display-time date-obj="[[entryObj.when]]"></display-time>
|
||||
<domain-icon domain="[[entryObj.domain]]" class='icon'></domain-icon>
|
||||
<div class='message' flex>
|
||||
<template is='dom-if' if="[[!entryObj.entityId]]">
|
||||
<span class='name'>[[entryObj.name]]</span>
|
||||
</template>
|
||||
<template is='dom-if' if="[[entryObj.entityId]]">
|
||||
<a href='#' on-click="entityClicked" class='name'>[[entryObj.name]]</a>
|
||||
<span> </span>
|
||||
</template>
|
||||
<span>[[entryObj.message]]</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
12
src/components/logbook-entry.js
Normal file
12
src/components/logbook-entry.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { moreInfoActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'logbook-entry',
|
||||
|
||||
entityClicked: function(ev) {
|
||||
ev.preventDefault();
|
||||
moreInfoActions.selectEntity(this.entryObj.entityId);
|
||||
}
|
||||
});
|
7
src/components/relative-ha-datetime.html
Normal file
7
src/components/relative-ha-datetime.html
Normal file
@ -0,0 +1,7 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id="relative-ha-datetime">
|
||||
<template>
|
||||
<span>[[relativeTime]]</span>
|
||||
</template>
|
||||
</dom-module>
|
61
src/components/relative-ha-datetime.js
Normal file
61
src/components/relative-ha-datetime.js
Normal file
@ -0,0 +1,61 @@
|
||||
import moment from 'moment';
|
||||
import { util } from 'home-assistant-js';
|
||||
import Polymer from '../polymer';
|
||||
|
||||
const UPDATE_INTERVAL = 60000; // 60 seconds
|
||||
|
||||
const { parseDateTime } = util;
|
||||
|
||||
export default Polymer({
|
||||
is: 'relative-ha-datetime',
|
||||
|
||||
properties: {
|
||||
datetime: {
|
||||
type: String,
|
||||
observer: 'datetimeChanged',
|
||||
},
|
||||
|
||||
datetimeObj: {
|
||||
type: Object,
|
||||
observer: 'datetimeObjChanged',
|
||||
},
|
||||
|
||||
parsedDateTime: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
relativeTime: {
|
||||
type: String,
|
||||
value: 'not set',
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.updateRelative = this.updateRelative.bind(this);
|
||||
},
|
||||
|
||||
attached() {
|
||||
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
|
||||
},
|
||||
|
||||
detached() {
|
||||
clearInterval(this._interval);
|
||||
},
|
||||
|
||||
datetimeChanged(newVal) {
|
||||
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
|
||||
|
||||
this.updateRelative();
|
||||
},
|
||||
|
||||
datetimeObjChanged(newVal) {
|
||||
this.parsedDateTime = newVal;
|
||||
|
||||
this.updateRelative();
|
||||
},
|
||||
|
||||
updateRelative() {
|
||||
this.relativeTime = this.parsedDateTime ?
|
||||
moment(this.parsedDateTime).fromNow() : "";
|
||||
},
|
||||
});
|
35
src/components/services-list.html
Normal file
35
src/components/services-list.html
Normal file
@ -0,0 +1,35 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/paper-menu/paper-menu.html">
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
|
||||
<dom-module id="services-list">
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<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.domain]]</span>/<span>[[service]]</span>
|
||||
</a></li>
|
||||
</template>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
</dom-module>
|
37
src/components/services-list.js
Normal file
37
src/components/services-list.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { serviceGetters } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('./domain-icon');
|
||||
|
||||
export default Polymer({
|
||||
is: 'services-list',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
serviceDomains: {
|
||||
type: Array,
|
||||
bindNuclear: [
|
||||
serviceGetters.entityMap,
|
||||
function(map) {
|
||||
return map.valueSeq()
|
||||
.sortBy(function(domain) { return domain.domain; })
|
||||
.toJS();
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
computeServices(domain) {
|
||||
return this.services.get(domain).toArray();
|
||||
},
|
||||
|
||||
serviceClicked(ev) {
|
||||
ev.preventDefault();
|
||||
this.fire(
|
||||
'service-selected', {domain: ev.model.domain.domain,
|
||||
service: ev.model.service});
|
||||
},
|
||||
});
|
52
src/components/state-badge.html
Normal file
52
src/components/state-badge.html
Normal file
@ -0,0 +1,52 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../../bower_components/iron-image/iron-image.html'>
|
||||
|
||||
<link rel='import' href='domain-icon.html'>
|
||||
|
||||
<dom-module id='state-badge'>
|
||||
<style>
|
||||
:host {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
background-color: #4fc3f7;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
div {
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
iron-image {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
domain-icon {
|
||||
margin: 0 auto;
|
||||
transition: color .3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Color the icon if light or sun is on */
|
||||
domain-icon[data-domain=light][data-state=on],
|
||||
domain-icon[data-domain=switch][data-state=on],
|
||||
domain-icon[data-domain=sun][data-state=above_horizon] {
|
||||
color: #fff176;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class='layout horizontal center'>
|
||||
<domain-icon id='icon'
|
||||
domain='[[stateObj.domain]]' data-domain$='[[stateObj.domain]]'
|
||||
state='[[stateObj.state]]' data-state$='[[stateObj.state]]'>
|
||||
</domain-icon>
|
||||
<template is='dom-if' if='[[stateObj.attributes.entity_picture]]'>
|
||||
<iron-image
|
||||
sizing='cover' class='fit'
|
||||
src$="[[stateObj.attributes.entity_picture]]"></iron-image>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
34
src/components/state-badge.js
Normal file
34
src/components/state-badge.js
Normal file
@ -0,0 +1,34 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
import xyBriToRgb from '../util/xybri-to-rgb';
|
||||
|
||||
require('./domain-icon');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-badge',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'updateIconColor',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when an attribute changes that influences the color of the icon.
|
||||
*/
|
||||
updateIconColor(newVal) {
|
||||
// for domain light, set color of icon to light color if available
|
||||
if(newVal.domain == "light" && newVal.state == "on" &&
|
||||
newVal.attributes.brightness && newVal.attributes.xy_color) {
|
||||
|
||||
var rgb = xyBriToRgb(newVal.attributes.xy_color[0],
|
||||
newVal.attributes.xy_color[1],
|
||||
newVal.attributes.brightness);
|
||||
this.$.icon.style.color = "rgb(" + rgb.map(Math.floor).join(",") + ")";
|
||||
} else {
|
||||
this.$.icon.style.color = null;
|
||||
}
|
||||
},
|
||||
|
||||
});
|
61
src/components/state-cards.html
Normal file
61
src/components/state-cards.html
Normal file
@ -0,0 +1,61 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../cards/state-card.html">
|
||||
|
||||
<dom-module id="state-cards">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media all and (min-width: 1020px) {
|
||||
.state-card {
|
||||
width: calc(50% - 44px);
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1356px) {
|
||||
.state-card {
|
||||
width: calc(33% - 38px);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1706px) {
|
||||
.state-card {
|
||||
width: calc(25% - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-states-content {
|
||||
max-width: 500px;
|
||||
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: 0 16px 8px;
|
||||
margin: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class='horizontal layout wrap'>
|
||||
|
||||
<template is='dom-repeat' items="{{states}}">
|
||||
<state-card class="state-card" state-obj="[[item]]"></state-card>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if="[[computeEmptyStates(states)]]">
|
||||
<div class='no-states-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>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
18
src/components/state-cards.js
Normal file
18
src/components/state-cards.js
Normal file
@ -0,0 +1,18 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('../cards/state-card');
|
||||
|
||||
Polymer({
|
||||
is: 'state-cards',
|
||||
|
||||
properties: {
|
||||
states: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
computeEmptyStates(states) {
|
||||
return states.length === 0;
|
||||
},
|
||||
});
|
1
src/components/state-history-chart-line.html
Normal file
1
src/components/state-history-chart-line.html
Normal file
@ -0,0 +1 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
194
src/components/state-history-chart-line.js
Normal file
194
src/components/state-history-chart-line.js
Normal file
@ -0,0 +1,194 @@
|
||||
import pluck from 'lodash/collection/pluck';
|
||||
import flatten from 'lodash/array/flatten';
|
||||
import uniq from 'lodash/array/uniq';
|
||||
import sortBy from 'lodash/collection/sortBy';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-history-chart-line',
|
||||
|
||||
properties: {
|
||||
data: {
|
||||
type: Object,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
|
||||
unit: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
isSingleDevice: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isAttached: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.style.display = 'block';
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.isAttached = true;
|
||||
},
|
||||
|
||||
dataChanged: function() {
|
||||
this.drawChart();
|
||||
},
|
||||
|
||||
/**************************************************
|
||||
The following code gererates line graphs for devices with continuous
|
||||
values(which are devices that have a unit_of_measurement values defined).
|
||||
On each graph the devices are grouped by their unit of measurement, eg. all
|
||||
sensors measuring MB will be a separate line on single graph. The google
|
||||
chart API takes data as a 2 dimensional array in the format:
|
||||
|
||||
DateTime, device1, device2, device3
|
||||
2015-04-01, 1, 2, 0
|
||||
2015-04-01, 0, 1, 0
|
||||
2015-04-01, 2, 1, 1
|
||||
|
||||
NOTE: the first column is a javascript date objects.
|
||||
|
||||
The first thing we do is build up the data with rows for each time of a state
|
||||
change and initialise the values to 0. THen we loop through each device and
|
||||
fill in its data.
|
||||
|
||||
**************************************************/
|
||||
drawChart() {
|
||||
if (!this.isAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
var root = Polymer.dom(this);
|
||||
var unit = this.unit;
|
||||
var deviceStates = this.data;
|
||||
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (deviceStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chart = new google.visualization.LineChart(this);
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
|
||||
dataTable.addColumn({ type: 'datetime', id: 'Time' });
|
||||
|
||||
var options = {
|
||||
legend: { position: 'top' },
|
||||
titlePosition: 'none',
|
||||
vAxes: {
|
||||
// Adds units to the left hand side of the graph
|
||||
0: {title: unit}
|
||||
},
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
lineWidth: 1,
|
||||
chartArea:{left:'60',width:"95%"},
|
||||
explorer: {
|
||||
actions: ['dragToZoom', 'rightClickToReset', 'dragToPan'],
|
||||
keepInBounds: true,
|
||||
axis: 'horizontal',
|
||||
maxZoomIn: 0.1
|
||||
}
|
||||
};
|
||||
|
||||
if(this.isSingleDevice) {
|
||||
options.legend.position = 'none';
|
||||
options.vAxes[0].title = null;
|
||||
options.chartArea.left = 40;
|
||||
options.chartArea.height = '80%';
|
||||
options.chartArea.top = 5;
|
||||
options.enableInteractivity = false;
|
||||
}
|
||||
|
||||
// Get a unique list of times of state changes for all the device
|
||||
// for a particular unit of measureent.
|
||||
var times = pluck(flatten(deviceStates), "lastChangedAsDate");
|
||||
times = uniq(times, function(e) {
|
||||
return e.getTime();
|
||||
});
|
||||
|
||||
times = sortBy(times, function(o) { return o; });
|
||||
|
||||
var data = [];
|
||||
var empty = new Array(deviceStates.length);
|
||||
for(var i = 0; i < empty.length; i++) {
|
||||
empty[i] = 0;
|
||||
}
|
||||
|
||||
var timeIndex = 1;
|
||||
var endDate = new Date();
|
||||
var prevDate = times[0];
|
||||
|
||||
for(i = 0; i < times.length; i++) {
|
||||
// because we only have state changes we add an extra point at the same time
|
||||
// that holds the previous state which makes the line display correctly
|
||||
var beforePoint = new Date(times[i]);
|
||||
data.push([beforePoint].concat(empty));
|
||||
|
||||
data.push([times[i]].concat(empty));
|
||||
prevDate = times[i];
|
||||
timeIndex++;
|
||||
}
|
||||
data.push([endDate].concat(empty));
|
||||
|
||||
|
||||
var deviceCount = 0;
|
||||
deviceStates.forEach(function(device) {
|
||||
var attributes = device[device.length - 1].attributes;
|
||||
dataTable.addColumn('number', attributes.friendly_name);
|
||||
|
||||
var currentState = 0;
|
||||
var previousState = 0;
|
||||
var lastIndex = 0;
|
||||
var count = 0;
|
||||
var prevTime = data[0][0];
|
||||
device.forEach(function(state) {
|
||||
|
||||
currentState = state.state;
|
||||
var start = state.lastChangedAsDate;
|
||||
if(state.state == 'None') {
|
||||
currentState = previousState;
|
||||
}
|
||||
for(var i = lastIndex; i < data.length; i++) {
|
||||
data[i][1 + deviceCount] = parseFloat(previousState);
|
||||
// this is where data gets filled in for each time for the particular device
|
||||
// because for each time two entries were create we fill the first one with the
|
||||
// previous value and the second one with the new value
|
||||
if(prevTime.getTime() == data[i][0].getTime() && data[i][0].getTime() == start.getTime()) {
|
||||
data[i][1 + deviceCount] = parseFloat(currentState);
|
||||
lastIndex = i;
|
||||
prevTime = data[i][0];
|
||||
break;
|
||||
}
|
||||
prevTime = data[i][0];
|
||||
}
|
||||
|
||||
previousState = currentState;
|
||||
|
||||
count++;
|
||||
}.bind(this));
|
||||
|
||||
//fill in the rest of the Array
|
||||
for(var i = lastIndex; i < data.length; i++) {
|
||||
data[i][1 + deviceCount] = parseFloat(previousState);
|
||||
}
|
||||
|
||||
deviceCount++;
|
||||
}.bind(this));
|
||||
|
||||
dataTable.addRows(data);
|
||||
chart.draw(dataTable, options);
|
||||
},
|
||||
});
|
10
src/components/state-history-chart-timeline.html
Normal file
10
src/components/state-history-chart-timeline.html
Normal file
@ -0,0 +1,10 @@
|
||||
<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>
|
107
src/components/state-history-chart-timeline.js
Normal file
107
src/components/state-history-chart-timeline.js
Normal file
@ -0,0 +1,107 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-history-chart-timeline',
|
||||
|
||||
properties: {
|
||||
data: {
|
||||
type: Object,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
|
||||
isAttached: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
},
|
||||
|
||||
attached() {
|
||||
this.isAttached = true;
|
||||
},
|
||||
|
||||
dataChanged() {
|
||||
this.drawChart();
|
||||
},
|
||||
|
||||
drawChart() {
|
||||
if (!this.isAttached) {
|
||||
return;
|
||||
}
|
||||
var root = Polymer.dom(this);
|
||||
var stateHistory = this.data;
|
||||
|
||||
while (root.node.lastChild) {
|
||||
root.node.removeChild(root.node.lastChild);
|
||||
}
|
||||
|
||||
if (!stateHistory || stateHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chart = new google.visualization.Timeline(this);
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
|
||||
dataTable.addColumn({ type: 'string', id: 'Entity' });
|
||||
dataTable.addColumn({ type: 'string', id: 'State' });
|
||||
dataTable.addColumn({ type: 'date', id: 'Start' });
|
||||
dataTable.addColumn({ type: 'date', id: 'End' });
|
||||
|
||||
var addRow = function(entityDisplay, stateStr, start, end) {
|
||||
stateStr = stateStr.replace(/_/g, ' ');
|
||||
dataTable.addRow([entityDisplay, stateStr, start, end]);
|
||||
};
|
||||
|
||||
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
|
||||
stateHistory.forEach(function(stateInfo) {
|
||||
if(stateInfo.length === 0) return;
|
||||
|
||||
var entityDisplay = stateInfo[0].entityDisplay;
|
||||
var newLastChanged, prevState = null, prevLastChanged = null;
|
||||
|
||||
stateInfo.forEach(function(state) {
|
||||
if (prevState !== null && state.state !== prevState) {
|
||||
newLastChanged = state.lastChangedAsDate;
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, newLastChanged);
|
||||
|
||||
prevState = state.state;
|
||||
prevLastChanged = newLastChanged;
|
||||
} else if (prevState === null) {
|
||||
prevState = state.state;
|
||||
prevLastChanged = state.lastChangedAsDate;
|
||||
}
|
||||
});
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, endTime);
|
||||
numTimelines++;
|
||||
}.bind(this));
|
||||
|
||||
chart.draw(dataTable, {
|
||||
height: 55 + numTimelines * 42,
|
||||
|
||||
timeline: {
|
||||
showRowLabels: stateHistory.length > 1
|
||||
},
|
||||
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
49
src/components/state-history-charts.html
Normal file
49
src/components/state-history-charts.html
Normal file
@ -0,0 +1,49 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/google-apis/google-legacy-loader.html">
|
||||
|
||||
<link rel="import" href="./loading-box.html">
|
||||
<link rel="import" href="./state-history-chart-timeline.html">
|
||||
<link rel="import" href="./state-history-chart-line.html">
|
||||
|
||||
<dom-module id="state-history-charts">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
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>Updating history data</loading-box>
|
||||
</div>
|
||||
|
||||
<div class$='[[computeContentClasses(isLoading)]]'>
|
||||
<template is='dom-if' if='[[computeIsEmpty(stateHistory)]]'>
|
||||
No state history found.
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
110
src/components/state-history-charts.js
Normal file
110
src/components/state-history-charts.js
Normal file
@ -0,0 +1,110 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./loading-box');
|
||||
require('./state-history-chart-timeline');
|
||||
require('./state-history-chart-line');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-history-charts',
|
||||
|
||||
properties: {
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isLoadingData: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
apiLoaded: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsLoading(isLoadingData, apiLoaded)',
|
||||
},
|
||||
|
||||
groupedStateHistory: {
|
||||
type: Object,
|
||||
computed: 'computeGroupedStateHistory(isLoading, stateHistory)',
|
||||
},
|
||||
|
||||
isSingleDevice: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSingleDevice(stateHistory)',
|
||||
},
|
||||
},
|
||||
|
||||
computeIsSingleDevice(stateHistory) {
|
||||
return stateHistory && stateHistory.size == 1;
|
||||
},
|
||||
|
||||
computeGroupedStateHistory(isLoading, stateHistory) {
|
||||
if (isLoading || !stateHistory) {
|
||||
return {line: [], timeline: []};
|
||||
}
|
||||
|
||||
var lineChartDevices = {};
|
||||
var timelineDevices = [];
|
||||
|
||||
stateHistory.forEach(function(stateInfo) {
|
||||
if (!stateInfo || stateInfo.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stateWithUnit = stateInfo.find(function(state) {
|
||||
return 'unit_of_measurement' in state.attributes;
|
||||
});
|
||||
|
||||
var unit = stateWithUnit ?
|
||||
stateWithUnit.attributes.unit_of_measurement : false;
|
||||
|
||||
if (!unit) {
|
||||
timelineDevices.push(stateInfo.toArray());
|
||||
} else if(unit in lineChartDevices) {
|
||||
lineChartDevices[unit].push(stateInfo.toArray());
|
||||
} else {
|
||||
lineChartDevices[unit] = [stateInfo.toArray()];
|
||||
}
|
||||
});
|
||||
|
||||
timelineDevices = timelineDevices.length > 0 && timelineDevices;
|
||||
|
||||
var unitStates = Object.keys(lineChartDevices).map(function(unit) {
|
||||
return [unit, lineChartDevices[unit]]; });
|
||||
|
||||
return {line: unitStates, timeline: timelineDevices};
|
||||
},
|
||||
|
||||
googleApiLoaded() {
|
||||
google.load("visualization", "1", {
|
||||
packages: ["timeline", "corechart"],
|
||||
callback: function() {
|
||||
this.apiLoaded = true;
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
computeContentClasses(isLoading) {
|
||||
return isLoading ? 'loading' : '';
|
||||
},
|
||||
|
||||
computeIsLoading(isLoadingData, apiLoaded) {
|
||||
return isLoadingData || !apiLoaded;
|
||||
},
|
||||
|
||||
computeIsEmpty(stateHistory) {
|
||||
return stateHistory && stateHistory.size === 0;
|
||||
},
|
||||
|
||||
extractUnit(arr) {
|
||||
return arr[0];
|
||||
},
|
||||
|
||||
extractData(arr) {
|
||||
return arr[1];
|
||||
},
|
||||
});
|
53
src/components/state-info.html
Normal file
53
src/components/state-info.html
Normal file
@ -0,0 +1,53 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-badge.html">
|
||||
<link rel="import" href="relative-ha-datetime.html">
|
||||
|
||||
<dom-module id="state-info">
|
||||
<style>
|
||||
:host {
|
||||
line-height: normal;
|
||||
min-width: 150px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
font-size: 1rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<state-badge state-obj='[[stateObj]]'></state-badge>
|
||||
|
||||
<div class='info'>
|
||||
<div class='name'>[[stateObj.entityDisplay]]</div>
|
||||
|
||||
<div class='time-ago'>
|
||||
<relative-ha-datetime datetime-obj='[[stateObj.lastChangedAsDate]]'></relative-ha-datetime>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
14
src/components/state-info.js
Normal file
14
src/components/state-info.js
Normal file
@ -0,0 +1,14 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./state-badge');
|
||||
require('./relative-ha-datetime');
|
||||
|
||||
export default Polymer({
|
||||
is: 'state-info',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
23
src/components/stream-status.html
Normal file
23
src/components/stream-status.html
Normal file
@ -0,0 +1,23 @@
|
||||
<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-toggle-button/paper-toggle-button.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/iron-icons/notification-icons.html">
|
||||
|
||||
<dom-module id="stream-status">
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
paper-toggle-button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<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>
|
30
src/components/stream-status.js
Normal file
30
src/components/stream-status.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { streamGetters, streamActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
export default Polymer({
|
||||
is: 'stream-status',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
bindNuclear: streamGetters.isStreamingEvents,
|
||||
},
|
||||
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
bindNuclear: streamGetters.hasStreamingEventsError,
|
||||
},
|
||||
},
|
||||
|
||||
toggleChanged: function() {
|
||||
if (this.isStreaming) {
|
||||
streamActions.stop();
|
||||
} else {
|
||||
streamActions.start();
|
||||
}
|
||||
},
|
||||
});
|
48
src/dialogs/more-info-dialog.html
Normal file
48
src/dialogs/more-info-dialog.html
Normal file
@ -0,0 +1,48 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
||||
<link rel="import" href="../../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html">
|
||||
<!-- <link rel="import" href="../../bower_components/neon-animation/animations/slide-up-animation.html">
|
||||
<link rel="import" href="../../bower_components/neon-animation/animations/slide-down-animation.html">
|
||||
-->
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
<link rel="import" href="../components/state-history-charts.html">
|
||||
<link rel="import" href="../more-infos/more-info-content.html">
|
||||
|
||||
<dom-module id="more-info-dialog">
|
||||
<style>
|
||||
state-card-content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
paper-dialog {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
|
||||
position: fixed !important;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<!-- entry-animation='slide-up-animation' exit-animation='slide-down-animation' -->
|
||||
<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]]">
|
||||
<state-history-charts state-history="[[stateHistory]]"
|
||||
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
|
||||
</template>
|
||||
<paper-dialog-scrollable>
|
||||
<more-info-content state-obj="[[stateObj]]"
|
||||
dialog-open="[[dialogOpen]]"></more-info-content>
|
||||
</paper-dialog-scrollable>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</template>
|
||||
</dom-module>
|
105
src/dialogs/more-info-dialog.js
Normal file
105
src/dialogs/more-info-dialog.js
Normal file
@ -0,0 +1,105 @@
|
||||
import {
|
||||
configGetters,
|
||||
entityHistoryGetters,
|
||||
entityHistoryActions,
|
||||
moreInfoGetters,
|
||||
moreInfoActions
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('../cards/state-card-content');
|
||||
require('../components/state-history-charts');
|
||||
require('../more-infos/more-info-content');
|
||||
|
||||
// if you don't want the history component to show add the domain to this array
|
||||
const DOMAINS_WITH_NO_HISTORY = ['camera'];
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-dialog',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
bindNuclear: moreInfoGetters.currentEntity,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
bindNuclear: [
|
||||
moreInfoGetters.currentEntityHistory,
|
||||
function(history) {
|
||||
return history ? [history] : false;
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
isLoadingHistoryData: {
|
||||
type: Boolean,
|
||||
bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
|
||||
},
|
||||
|
||||
hasHistoryComponent: {
|
||||
type: Boolean,
|
||||
bindNuclear: configGetters.isComponentLoaded('history'),
|
||||
observer: 'fetchHistoryData',
|
||||
},
|
||||
|
||||
shouldFetchHistory: {
|
||||
type: Boolean,
|
||||
bindNuclear: moreInfoGetters.isCurrentEntityHistoryStale,
|
||||
observer: 'fetchHistoryData',
|
||||
},
|
||||
|
||||
showHistoryComponent: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
dialogOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dialogOpenChanged',
|
||||
},
|
||||
},
|
||||
|
||||
fetchHistoryData() {
|
||||
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) {
|
||||
this.showHistoryComponent = false;
|
||||
}
|
||||
else {
|
||||
this.showHistoryComponent = this.hasHistoryComponent;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged(newVal) {
|
||||
if (!newVal) {
|
||||
this.dialogOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchHistoryData();
|
||||
|
||||
// allow dialog to render content before showing it so it is
|
||||
// positioned correctly.
|
||||
this.async(function() {
|
||||
this.dialogOpen = true;
|
||||
}.bind(this), 10);
|
||||
},
|
||||
|
||||
dialogOpenChanged(newVal) {
|
||||
if (!newVal) {
|
||||
moreInfoActions.deselectEntity();
|
||||
}
|
||||
},
|
||||
});
|
32
src/home-assistant.html
Normal file
32
src/home-assistant.html
Normal file
@ -0,0 +1,32 @@
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel='import' href='../bower_components/paper-styles/typography.html'>
|
||||
|
||||
<link rel='import' href='resources/home-assistant-icons.html'>
|
||||
<link rel='import' href='resources/home-assistant-style.html'>
|
||||
|
||||
<link rel='import' href='layouts/login-form.html'>
|
||||
<link rel='import' href='layouts/home-assistant-main.html'>
|
||||
|
||||
<dom-module id='home-assistant'>
|
||||
<style>
|
||||
:host {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<template is='dom-if' if='[[!loaded]]'>
|
||||
<login-form></login-form>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[loaded]]'>
|
||||
<home-assistant-main></home-assistant-main>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script src='../build/_app_compiled.js'></script>
|
46
src/home-assistant.js
Normal file
46
src/home-assistant.js
Normal file
@ -0,0 +1,46 @@
|
||||
import Polymer from './polymer';
|
||||
|
||||
import {
|
||||
syncGetters,
|
||||
localStoragePreferences
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import nuclearObserver from './util/bound-nuclear-behavior';
|
||||
import validateAuth from './util/validate-auth';
|
||||
|
||||
require('./layouts/login-form');
|
||||
require('./layouts/home-assistant-main');
|
||||
|
||||
export default Polymer({
|
||||
is: 'home-assistant',
|
||||
|
||||
hostAttributes: {
|
||||
auth: null,
|
||||
},
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
auth: {
|
||||
type: String,
|
||||
},
|
||||
loaded: {
|
||||
type: Boolean,
|
||||
bindNuclear: syncGetters.isDataLoaded,
|
||||
},
|
||||
},
|
||||
|
||||
ready() {
|
||||
// remove the HTML init message
|
||||
document.getElementById('init').remove();
|
||||
|
||||
// if auth was given, tell the backend
|
||||
if(this.auth) {
|
||||
validateAuth(this.auth, false);
|
||||
} else if (localStoragePreferences.authToken) {
|
||||
validateAuth(localStoragePreferences.authToken, true);
|
||||
}
|
||||
|
||||
localStoragePreferences.startSync();
|
||||
},
|
||||
});
|
46
src/layouts/home-assistant-main.html
Normal file
46
src/layouts/home-assistant-main.html
Normal file
@ -0,0 +1,46 @@
|
||||
<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-drawer-panel/paper-drawer-panel.html'>
|
||||
<link rel='import' href='../layouts/partial-states.html'>
|
||||
<link rel='import' href='../layouts/partial-logbook.html'>
|
||||
<link rel='import' href='../layouts/partial-history.html'>
|
||||
<link rel='import' href='../layouts/partial-dev-call-service.html'>
|
||||
<link rel='import' href='../layouts/partial-dev-fire-event.html'>
|
||||
<link rel='import' href='../layouts/partial-dev-set-state.html'>
|
||||
<link rel='import' href='../managers/notification-manager.html'>
|
||||
<link rel="import" href="../dialogs/more-info-dialog.html">
|
||||
|
||||
<link rel='import' href='../components/ha-sidebar.html'>
|
||||
|
||||
<dom-module id='home-assistant-main'>
|
||||
<template>
|
||||
<notification-manager></notification-manager>
|
||||
<more-info-dialog></more-info-dialog>
|
||||
|
||||
<paper-drawer-panel id='drawer' narrow='{{narrow}}'>
|
||||
<ha-sidebar drawer></ha-sidebar>
|
||||
|
||||
<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>
|
||||
<template is='dom-if' if='[[isSelectedHistory]]'>
|
||||
<partial-history main narrow='[[narrow]]'></partial-history>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedDevService]]'>
|
||||
<partial-dev-call-service main narrow='[[narrow]]'></partial-dev-call-service>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedDevEvent]]'>
|
||||
<partial-dev-fire-event main narrow='[[narrow]]'></partial-dev-fire-event>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedDevState]]'>
|
||||
<partial-dev-set-state main narrow='[[narrow]]'></partial-dev-set-state>
|
||||
</template>
|
||||
</paper-drawer-panel>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
89
src/layouts/home-assistant-main.js
Normal file
89
src/layouts/home-assistant-main.js
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
configGetters,
|
||||
entityGetters,
|
||||
navigationGetters,
|
||||
authActions,
|
||||
navigationActions,
|
||||
urlSync,
|
||||
util
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('../components/ha-sidebar');
|
||||
require('../layouts/partial-states');
|
||||
require('../layouts/partial-logbook');
|
||||
require('../layouts/partial-history');
|
||||
require('../layouts/partial-dev-call-service');
|
||||
require('../layouts/partial-dev-fire-event');
|
||||
require('../layouts/partial-dev-set-state');
|
||||
require('../managers/notification-manager');
|
||||
require('../dialogs/more-info-dialog');
|
||||
|
||||
export default Polymer({
|
||||
is: 'home-assistant-main',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
activePage: {
|
||||
type: String,
|
||||
bindNuclear: navigationGetters.activePage,
|
||||
observer: 'activePageChanged',
|
||||
},
|
||||
|
||||
isSelectedStates: {
|
||||
type: Boolean,
|
||||
bindNuclear: navigationGetters.isActivePane('states'),
|
||||
},
|
||||
|
||||
isSelectedHistory: {
|
||||
type: Boolean,
|
||||
bindNuclear: navigationGetters.isActivePane('history'),
|
||||
},
|
||||
|
||||
isSelectedLogbook: {
|
||||
type: Boolean,
|
||||
bindNuclear: navigationGetters.isActivePane('logbook'),
|
||||
},
|
||||
|
||||
isSelectedDevEvent: {
|
||||
type: Boolean,
|
||||
bindNuclear: navigationGetters.isActivePane('devEvent'),
|
||||
},
|
||||
|
||||
isSelectedDevState: {
|
||||
type: Boolean,
|
||||
bindNuclear: navigationGetters.isActivePane('devState'),
|
||||
},
|
||||
|
||||
isSelectedDevService: {
|
||||
type: Boolean,
|
||||
bindNuclear: navigationGetters.isActivePane('devService'),
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'open-menu': 'openDrawer',
|
||||
},
|
||||
|
||||
openDrawer: function() {
|
||||
this.$.drawer.openDrawer();
|
||||
},
|
||||
|
||||
activePageChanged: function() {
|
||||
this.$.drawer.closeDrawer();
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
urlSync.startSync();
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
urlSync.stopSync();
|
||||
},
|
||||
});
|
83
src/layouts/login-form.html
Normal file
83
src/layouts/login-form.html
Normal file
@ -0,0 +1,83 @@
|
||||
<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-checkbox/paper-checkbox.html">
|
||||
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input-container.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input-error.html">
|
||||
<link rel="import" href="../../bower_components/iron-input/iron-input.html">
|
||||
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<dom-module id="login-form">
|
||||
<style>
|
||||
:host {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#passwordDecorator {
|
||||
display: block;
|
||||
height: 57px;
|
||||
}
|
||||
|
||||
paper-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
paper-checkbox::shadow #checkbox.checked {
|
||||
background-color: #03a9f4;
|
||||
border-color: #03a9f4;
|
||||
}
|
||||
|
||||
paper-checkbox::shadow #ink[checked] {
|
||||
color: #03a9f4;
|
||||
}
|
||||
|
||||
paper-button {
|
||||
margin-left: 72px;
|
||||
}
|
||||
|
||||
.interact {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
#validatebox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validatemessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="layout horizontal center fit login" id="splash">
|
||||
<div class="layout vertical center flex">
|
||||
|
||||
<img src="/static/favicon-192x192.png" />
|
||||
<h1>Home Assistant</h1>
|
||||
|
||||
<a href="#" id="hideKeyboardOnFocus"></a>
|
||||
|
||||
<div class='interact'>
|
||||
<div id='loginform' hidden$="[[isValidating]]">
|
||||
<paper-input-container id="passwordDecorator" invalid="[[isInvalid]]">
|
||||
<label>Password</label>
|
||||
<input is="iron-input" type="password" id="passwordInput" />
|
||||
<paper-input-error invalid="[[isInvalid]]">[[errorMessage]]</paper-input-error>
|
||||
</paper-input-container>
|
||||
|
||||
<div class="layout horizontal center">
|
||||
<paper-checkbox for id='rememberLogin'>Remember</paper-checkbox>
|
||||
<paper-button id='loginButton'>Log In</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="validatebox" hidden$="[[!isValidating]]">
|
||||
<paper-spinner active="true"></paper-spinner><br />
|
||||
<div class="validatemessage">Loading data…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
69
src/layouts/login-form.js
Normal file
69
src/layouts/login-form.js
Normal file
@ -0,0 +1,69 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
import { authGetters } from 'home-assistant-js';
|
||||
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
import validateAuth from '../util/validate-auth';
|
||||
|
||||
export default Polymer({
|
||||
is: 'login-form',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
isValidating: {
|
||||
type: Boolean,
|
||||
observer: 'isValidatingChanged',
|
||||
bindNuclear: authGetters.isValidating,
|
||||
},
|
||||
|
||||
isInvalid: {
|
||||
type: Boolean,
|
||||
bindNuclear: authGetters.isInvalidAttempt,
|
||||
},
|
||||
|
||||
errorMessage: {
|
||||
type: String,
|
||||
bindNuclear: authGetters.attemptErrorMessage,
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'keydown': 'passwordKeyDown',
|
||||
'loginButton.click': 'validatePassword',
|
||||
},
|
||||
|
||||
observers: [
|
||||
'validatingChanged(isValidating, isInvalid)',
|
||||
],
|
||||
|
||||
validatingChanged: function(isValidating, isInvalid) {
|
||||
if (!isValidating && !isInvalid) {
|
||||
this.$.passwordInput.value = '';
|
||||
}
|
||||
},
|
||||
|
||||
isValidatingChanged: function(newVal) {
|
||||
if (!newVal) {
|
||||
this.async(function() { this.$.passwordInput.focus(); }.bind(this), 10);
|
||||
}
|
||||
},
|
||||
|
||||
passwordKeyDown: function(ev) {
|
||||
// validate on enter
|
||||
if(ev.keyCode === 13) {
|
||||
this.validatePassword();
|
||||
ev.preventDefault();
|
||||
|
||||
// clear error after we start typing again
|
||||
} else if(this.isInvalid) {
|
||||
this.isInvalid = false;
|
||||
}
|
||||
},
|
||||
|
||||
validatePassword: function() {
|
||||
this.$.hideKeyboardOnFocus.focus();
|
||||
|
||||
validateAuth(this.$.passwordInput.value, this.$.rememberLogin.checked);
|
||||
},
|
||||
});
|
29
src/layouts/partial-base.html
Normal file
29
src/layouts/partial-base.html
Normal file
@ -0,0 +1,29 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel='import' href='../../bower_components/paper-scroll-header-panel/paper-scroll-header-panel.html'>
|
||||
|
||||
<link rel='import' href='../../bower_components/paper-toolbar/paper-toolbar.html'>
|
||||
<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 class="title">
|
||||
<content select='[header-title]'></content>
|
||||
</div>
|
||||
<content select='[header-buttons]'></content>
|
||||
</paper-toolbar>
|
||||
|
||||
<content></content>
|
||||
</paper-scroll-header-panel>
|
||||
</template>
|
||||
</dom-module>
|
16
src/layouts/partial-base.js
Normal file
16
src/layouts/partial-base.js
Normal file
@ -0,0 +1,16 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-base',
|
||||
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
toggleMenu: function() {
|
||||
this.fire('open-menu');
|
||||
},
|
||||
});
|
47
src/layouts/partial-dev-call-service.html
Normal file
47
src/layouts/partial-dev-call-service.html
Normal file
@ -0,0 +1,47 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/services-list.html">
|
||||
|
||||
<dom-module id="partial-dev-call-service">
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>Call Service</span>
|
||||
|
||||
<div class='form fit'>
|
||||
<p>
|
||||
Call a service from a component.
|
||||
</p>
|
||||
|
||||
<div class$='[[computeFormClasses(narrow)]]'>
|
||||
<div class='ha-form flex'>
|
||||
<paper-input label="Domain" autofocus value='{{domain}}'></paper-input>
|
||||
<paper-input label="Service" value='{{service}}'></paper-input>
|
||||
<paper-textarea label="Service Data (JSON, optional)" value='{{serviceData}}'></paper-textarea>
|
||||
<paper-button on-click='callService' raised>Call Service</paper-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Available services:</h4>
|
||||
<services-list on-service-selected='serviceSelected'></services-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
54
src/layouts/partial-dev-call-service.js
Normal file
54
src/layouts/partial-dev-call-service.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { serviceActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./partial-base');
|
||||
require('../components/services-list');
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-dev-call-service',
|
||||
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
domain: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
service: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
serviceData: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
serviceSelected(ev) {
|
||||
this.domain = ev.detail.domain;
|
||||
this.service = ev.detail.service;
|
||||
},
|
||||
|
||||
callService() {
|
||||
var serviceData;
|
||||
|
||||
try {
|
||||
serviceData = this.serviceData ? JSON.parse(this.serviceData): {};
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
serviceActions.callService(this.domain, this.service, serviceData);
|
||||
},
|
||||
|
||||
computeFormClasses(narrow) {
|
||||
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
|
||||
},
|
||||
});
|
48
src/layouts/partial-dev-fire-event.html
Normal file
48
src/layouts/partial-dev-fire-event.html
Normal file
@ -0,0 +1,48 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/events-list.html">
|
||||
|
||||
<dom-module id="partial-dev-fire-event">
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<partial-base narrow="{{narrow}}">
|
||||
<span header-title>Fire Event</span>
|
||||
|
||||
<div class='form fit'>
|
||||
<p>
|
||||
Fire an event on the event bus.
|
||||
</p>
|
||||
|
||||
<div class$='[[computeFormClasses(narrow)]]'>
|
||||
<div class='ha-form flex'>
|
||||
<paper-input label="Event Type" autofocus required value='{{eventType}}'></paper-input>
|
||||
<paper-textarea label="Event Data (JSON, optional)" value='{{eventData}}'></paper-textarea>
|
||||
<paper-button on-click='fireEvent' raised>Fire Event</paper-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Available events:</h4>
|
||||
<events-list on-event-selected='eventSelected'></event-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
43
src/layouts/partial-dev-fire-event.js
Normal file
43
src/layouts/partial-dev-fire-event.js
Normal file
@ -0,0 +1,43 @@
|
||||
import { eventActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./partial-base');
|
||||
require('../components/events-list');
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-dev-fire-event',
|
||||
|
||||
properties: {
|
||||
eventType: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
eventData: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
eventSelected(ev) {
|
||||
this.eventType = ev.detail.eventType;
|
||||
},
|
||||
|
||||
fireEvent() {
|
||||
var eventData;
|
||||
|
||||
try {
|
||||
eventData = this.eventData ? JSON.parse(this.eventData) : {};
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
eventActions.fireEvent(this.eventType, eventData);
|
||||
},
|
||||
|
||||
computeFormClasses(narrow) {
|
||||
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
|
||||
},
|
||||
});
|
49
src/layouts/partial-dev-set-state.html
Normal file
49
src/layouts/partial-dev-set-state.html
Normal file
@ -0,0 +1,49 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/entity-list.html">
|
||||
|
||||
<dom-module id="partial-dev-set-state">
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>Set State</span>
|
||||
|
||||
<div class='form fit'>
|
||||
<div>
|
||||
Set the representation of a device within Home Assistant.<br />
|
||||
This will not communicate with the actual device.
|
||||
</div>
|
||||
|
||||
<div class$='[[computeFormClasses(narrow)]]'>
|
||||
<div class='ha-form flex'>
|
||||
<paper-input label="Entity ID" autofocus required value='{{entityId}}'></paper-input>
|
||||
<paper-input label="State" required value='{{state}}'></paper-input>
|
||||
<paper-textarea label="State attributes (JSON, optional)" value='{{stateAttributes}}'></paper-textarea>
|
||||
<paper-button on-click='handleSetState' raised>Set State</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<h4>Current entities:</h4>
|
||||
<entity-list on-entity-selected='entitySelected'></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
64
src/layouts/partial-dev-set-state.js
Normal file
64
src/layouts/partial-dev-set-state.js
Normal file
@ -0,0 +1,64 @@
|
||||
import { reactor, entityGetters, entityActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
|
||||
require('./partial-base');
|
||||
require('../components/entity-list');
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-dev-set-state',
|
||||
|
||||
properties: {
|
||||
entityId: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
state: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
stateAttributes: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
setStateData(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
|
||||
this.$.inputData.value = value;
|
||||
|
||||
// not according to the spec but it works...
|
||||
this.$.inputDataWrapper.update(this.$.inputData);
|
||||
},
|
||||
|
||||
entitySelected(ev) {
|
||||
var state = reactor.evaluate(entityGetters.byId(ev.detail.entityId));
|
||||
|
||||
this.entityId = state.entityId;
|
||||
this.state = state.state;
|
||||
this.stateAttributes = JSON.stringify(state.attributes, null, ' ');
|
||||
},
|
||||
|
||||
handleSetState() {
|
||||
var attr;
|
||||
try {
|
||||
attr = this.stateAttributes ? JSON.parse(this.stateAttributes) : {};
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
entityActions.save({
|
||||
entityId: this.entityId,
|
||||
state: this.state,
|
||||
attributes: attr,
|
||||
});
|
||||
},
|
||||
|
||||
computeFormClasses(narrow) {
|
||||
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
|
||||
},
|
||||
});
|
45
src/layouts/partial-history.html
Normal file
45
src/layouts/partial-history.html
Normal file
@ -0,0 +1,45 @@
|
||||
<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 {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.content.wide {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
paper-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.narrow paper-input {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>History</span>
|
||||
|
||||
<paper-icon-button icon="refresh" header-buttons
|
||||
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>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
75
src/layouts/partial-history.js
Normal file
75
src/layouts/partial-history.js
Normal file
@ -0,0 +1,75 @@
|
||||
import {
|
||||
uiActions,
|
||||
entityHistoryGetters,
|
||||
entityHistoryActions
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('./partial-base');
|
||||
require('../components/state-history-charts');
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-history',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
isDataLoaded: {
|
||||
type: Boolean,
|
||||
bindNuclear: entityHistoryGetters.hasDataForCurrentDate,
|
||||
observer: 'isDataLoadedChanged',
|
||||
},
|
||||
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
bindNuclear: entityHistoryGetters.entityHistoryForCurrentDate,
|
||||
},
|
||||
|
||||
isLoadingData: {
|
||||
type: Boolean,
|
||||
bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
|
||||
},
|
||||
|
||||
selectedDate: {
|
||||
type: String,
|
||||
value: null,
|
||||
bindNuclear: entityHistoryGetters.currentDate,
|
||||
},
|
||||
},
|
||||
|
||||
isDataLoadedChanged(newVal) {
|
||||
if (!newVal) {
|
||||
entityHistoryActions.fetchSelectedDate();
|
||||
}
|
||||
},
|
||||
|
||||
handleRefreshClick() {
|
||||
entityHistoryActions.fetchSelectedDate();
|
||||
},
|
||||
|
||||
datepickerFocus() {
|
||||
this.datePicker.adjustPosition();
|
||||
this.datePicker.gotoDate(moment('2015-06-30').toDate());
|
||||
},
|
||||
|
||||
attached() {
|
||||
this.datePicker = new Pikaday({
|
||||
field: this.$.datePicker.inputElement,
|
||||
onSelect: entityHistoryActions.changeCurrentDate,
|
||||
});
|
||||
},
|
||||
|
||||
detached() {
|
||||
this.datePicker.destroy();
|
||||
},
|
||||
|
||||
computeContentClasses(narrow) {
|
||||
return 'flex content ' + (narrow ? 'narrow' : 'wide');
|
||||
},
|
||||
});
|
41
src/layouts/partial-logbook.html
Normal file
41
src/layouts/partial-logbook.html
Normal file
@ -0,0 +1,41 @@
|
||||
<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>
|
||||
.selected-date-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
paper-input {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>Logbook</span>
|
||||
|
||||
<paper-icon-button icon="refresh" header-buttons
|
||||
on-click="handleRefresh"></paper-icon-button>
|
||||
|
||||
<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>
|
77
src/layouts/partial-logbook.js
Normal file
77
src/layouts/partial-logbook.js
Normal file
@ -0,0 +1,77 @@
|
||||
import { logbookGetters, logbookActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('./partial-base');
|
||||
require('../components/ha-logbook');
|
||||
require('../components/loading-box');
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-logbook',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
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,
|
||||
bindNuclear: [
|
||||
logbookGetters.currentEntries,
|
||||
function(entries) { return entries.toArray(); },
|
||||
],
|
||||
},
|
||||
|
||||
datePicker: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
isStaleChanged(newVal) {
|
||||
if (newVal) {
|
||||
// isLoading wouldn't update without async <_<
|
||||
this.async(
|
||||
function() { logbookActions.fetchDate(this.selectedDate); }, 10);
|
||||
}
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
logbookActions.fetchDate(this.selectedDate);
|
||||
},
|
||||
|
||||
datepickerFocus() {
|
||||
this.datePicker.adjustPosition();
|
||||
this.datePicker.gotoDate(moment('2015-06-30').toDate());
|
||||
},
|
||||
|
||||
attached() {
|
||||
this.datePicker = new Pikaday({
|
||||
field: this.$.datePicker.inputElement,
|
||||
onSelect: logbookActions.changeCurrentDate,
|
||||
});
|
||||
},
|
||||
|
||||
detached() {
|
||||
this.datePicker.destroy();
|
||||
},
|
||||
});
|
76
src/layouts/partial-states.html
Normal file
76
src/layouts/partial-states.html
Normal file
@ -0,0 +1,76 @@
|
||||
<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="./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>
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: #E5E5E5;
|
||||
}
|
||||
|
||||
.content-wrapper ::content .listening {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
|
||||
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;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.interimTranscript {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.listening paper-spinner {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<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="[[computeListenButtonIcon(isListening)]]"
|
||||
hidden$='[[!canListen]]'
|
||||
on-click="handleListenClick"></paper-icon-button>
|
||||
</span>
|
||||
|
||||
<div class='content-wrapper'>
|
||||
<div class='listening' hidden$="[[!showListenInterface]]"
|
||||
on-click="handleListenClick">
|
||||
<ha-voice-command-progress></ha-voice-command-progress>
|
||||
</div>
|
||||
|
||||
<state-cards states="[[states]]">
|
||||
<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>
|
||||
</state-cards>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
|
||||
</dom-module>
|
112
src/layouts/partial-states.js
Normal file
112
src/layouts/partial-states.js
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
configGetters,
|
||||
navigationGetters,
|
||||
voiceGetters,
|
||||
streamGetters,
|
||||
serviceGetters,
|
||||
syncGetters,
|
||||
syncActions,
|
||||
voiceActions,
|
||||
util
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
const { entityDomainFilters } = util;
|
||||
|
||||
require('./partial-base');
|
||||
require('../components/state-cards');
|
||||
require('../components/ha-voice-command-progress');
|
||||
|
||||
export default Polymer({
|
||||
is: 'partial-states',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
filter: {
|
||||
type: String,
|
||||
bindNuclear: navigationGetters.activeFilter,
|
||||
},
|
||||
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
bindNuclear: syncGetters.isFetching,
|
||||
},
|
||||
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
bindNuclear: streamGetters.isStreamingEvents,
|
||||
},
|
||||
|
||||
canListen: {
|
||||
type: Boolean,
|
||||
bindNuclear: [
|
||||
voiceGetters.isVoiceSupported,
|
||||
configGetters.isComponentLoaded('conversation'),
|
||||
function(isVoiceSupported, componentLoaded) {
|
||||
return isVoiceSupported && componentLoaded;
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
isListening: {
|
||||
type: Boolean,
|
||||
bindNuclear: voiceGetters.isListening,
|
||||
},
|
||||
|
||||
showListenInterface: {
|
||||
type: Boolean,
|
||||
bindNuclear: [
|
||||
voiceGetters.isListening,
|
||||
voiceGetters.isTransmitting,
|
||||
function(isListening, isTransmitting) {
|
||||
return isListening || isTransmitting;
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
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() {
|
||||
syncActions.fetchAll();
|
||||
},
|
||||
|
||||
handleListenClick() {
|
||||
if (this.isListening) {
|
||||
voiceActions.stop();
|
||||
} else {
|
||||
voiceActions.listen();
|
||||
}
|
||||
},
|
||||
|
||||
computeHeaderTitle(filter) {
|
||||
return filter ? entityDomainFilters[filter] : 'States';
|
||||
},
|
||||
|
||||
computeListenButtonIcon(isListening) {
|
||||
return isListening ? 'av:mic-off' : 'av:mic';
|
||||
},
|
||||
|
||||
computeRefreshButtonClass(isFetching) {
|
||||
if (isFetching) {
|
||||
return 'ha-spin';
|
||||
}
|
||||
},
|
||||
});
|
14
src/managers/notification-manager.html
Normal file
14
src/managers/notification-manager.html
Normal file
@ -0,0 +1,14 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../../bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<dom-module id="notification-manager">
|
||||
<style>
|
||||
paper-toast {
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<paper-toast id="toast" text='{{text}}'></paper-toast>
|
||||
</template>
|
||||
</dom-module>
|
24
src/managers/notification-manager.js
Normal file
24
src/managers/notification-manager.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { notificationGetters } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
export default Polymer({
|
||||
is: 'notification-manager',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
text: {
|
||||
type: String,
|
||||
bindNuclear: notificationGetters.lastNotificationMessage,
|
||||
observer: 'showNotification',
|
||||
},
|
||||
},
|
||||
|
||||
showNotification(newText) {
|
||||
if (newText) {
|
||||
this.$.toast.show();
|
||||
}
|
||||
}
|
||||
});
|
18
src/more-infos/more-info-camera.html
Normal file
18
src/more-infos/more-info-camera.html
Normal file
@ -0,0 +1,18 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id='more-info-camera'>
|
||||
<style>
|
||||
:host {
|
||||
max-width:640px;
|
||||
}
|
||||
|
||||
.camera-image {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<img class='camera-image' src="[[computeCameraImageUrl(dialogOpen)]]"
|
||||
on-load='imageLoaded' />
|
||||
</template>
|
||||
</dom-module>
|
29
src/more-infos/more-info-camera.js
Normal file
29
src/more-infos/more-info-camera.js
Normal file
@ -0,0 +1,29 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-camera',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
dialogOpen: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
|
||||
imageLoaded() {
|
||||
this.fire('iron-resize');
|
||||
},
|
||||
|
||||
computeCameraImageUrl(dialogOpen) {
|
||||
if (__DEMO__) {
|
||||
return 'http://194.218.96.92/jpg/image.jpg';
|
||||
} else if (dialogOpen) {
|
||||
return '/api/camera_proxy_stream/' + this.stateObj.entityId;
|
||||
} else {
|
||||
// Return an empty image if dialog is not open
|
||||
return 'data:image/gif;base64,R0lGODlhAQABAAAAACw=';
|
||||
}
|
||||
},
|
||||
});
|
51
src/more-infos/more-info-configurator.html
Normal file
51
src/more-infos/more-info-configurator.html
Normal file
@ -0,0 +1,51 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../../bower_components/paper-button/paper-button.html'>
|
||||
|
||||
<link rel='import' href='../components/loading-box.html'>
|
||||
|
||||
<dom-module id='more-info-configurator'>
|
||||
<style>
|
||||
p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
p > img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.error {
|
||||
color: #C62828;
|
||||
}
|
||||
|
||||
p.submit {
|
||||
text-align: center;
|
||||
height: 41px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='layout vertical'>
|
||||
<template is='dom-if' if='[[isConfigurable]]'>
|
||||
|
||||
<p hidden$='[[!stateObj.attributes.description]]'>[[stateObj.attributes.description]]</p>
|
||||
|
||||
<p class='error' hidden$='[[!stateObj.attributes.errors]]'>[[stateObj.attributes.errors]]</p>
|
||||
|
||||
<p class='center' hidden$='[[!stateObj.attributes.description_image]]'>
|
||||
<img src='[[stateObj.attributes.description_image]]' />
|
||||
</p>
|
||||
|
||||
<p class='submit'>
|
||||
<paper-button raised on-click='submitClicked'
|
||||
hidden$='[[isConfiguring]]'>[[submitCaption]]</paper-button>
|
||||
|
||||
<loading-box hidden$='[[!isConfiguring]]'>Configuring</loading-box>
|
||||
</p>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
76
src/more-infos/more-info-configurator.js
Normal file
76
src/more-infos/more-info-configurator.js
Normal file
@ -0,0 +1,76 @@
|
||||
import {
|
||||
streamGetters,
|
||||
syncActions,
|
||||
serviceActions
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('../components/loading-box');
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-configurator',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
action: {
|
||||
type: String,
|
||||
value: 'display',
|
||||
},
|
||||
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
bindNuclear: streamGetters.isStreamingEvents,
|
||||
},
|
||||
|
||||
isConfigurable: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsConfigurable(stateObj)',
|
||||
},
|
||||
|
||||
isConfiguring: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
submitCaption: {
|
||||
type: String,
|
||||
computed: 'computeSubmitCaption(stateObj)',
|
||||
},
|
||||
},
|
||||
|
||||
computeIsConfigurable(stateObj) {
|
||||
return stateObj.state == 'configure';
|
||||
},
|
||||
|
||||
computeSubmitCaption(stateObj) {
|
||||
return stateObj.attributes.submit_caption || 'Set configuration';
|
||||
},
|
||||
|
||||
submitClicked() {
|
||||
this.isConfiguring = true;
|
||||
|
||||
var data = {
|
||||
configure_id: this.stateObj.attributes.configure_id
|
||||
};
|
||||
|
||||
serviceActions.callService('configurator', 'configure', data).then(
|
||||
function() {
|
||||
this.isConfiguring = false;
|
||||
|
||||
if (!this.isStreaming) {
|
||||
syncActions.fetchAll();
|
||||
}
|
||||
}.bind(this),
|
||||
|
||||
function() {
|
||||
this.isConfiguring = false;
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
19
src/more-infos/more-info-content.html
Normal file
19
src/more-infos/more-info-content.html
Normal file
@ -0,0 +1,19 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel='import' href='more-info-default.html'>
|
||||
<link rel='import' href='more-info-group.html'>
|
||||
<link rel='import' href='more-info-sun.html'>
|
||||
<link rel='import' href='more-info-configurator.html'>
|
||||
<link rel='import' href='more-info-thermostat.html'>
|
||||
<link rel='import' href='more-info-script.html'>
|
||||
<link rel='import' href='more-info-light.html'>
|
||||
<link rel='import' href='more-info-media_player.html'>
|
||||
<link rel='import' href='more-info-camera.html'>
|
||||
|
||||
<dom-module id='more-info-content'>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</dom-module>
|
68
src/more-infos/more-info-content.js
Normal file
68
src/more-infos/more-info-content.js
Normal file
@ -0,0 +1,68 @@
|
||||
import Polymer from '../polymer';
|
||||
import stateMoreInfoType from '../util/state-more-info-type';
|
||||
|
||||
require('./more-info-default');
|
||||
require('./more-info-group');
|
||||
require('./more-info-sun');
|
||||
require('./more-info-configurator');
|
||||
require('./more-info-thermostat');
|
||||
require('./more-info-script');
|
||||
require('./more-info-light');
|
||||
require('./more-info-media_player');
|
||||
require('./more-info-camera');
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-content',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
dialogOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dialogOpenChanged',
|
||||
},
|
||||
},
|
||||
|
||||
dialogOpenChanged(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (root.lastChild) {
|
||||
root.lastChild.dialogOpen = newVal;
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (!newVal) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var newMoreInfoType = stateMoreInfoType(newVal);
|
||||
|
||||
if (!oldVal || stateMoreInfoType(oldVal) != newMoreInfoType) {
|
||||
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
var moreInfo = document.createElement('more-info-' + newMoreInfoType);
|
||||
moreInfo.stateObj = newVal;
|
||||
moreInfo.dialogOpen = this.dialogOpen;
|
||||
root.appendChild(moreInfo);
|
||||
|
||||
} else {
|
||||
|
||||
root.lastChild.dialogOpen = this.dialogOpen;
|
||||
root.lastChild.stateObj = newVal;
|
||||
|
||||
}
|
||||
},
|
||||
});
|
19
src/more-infos/more-info-default.html
Normal file
19
src/more-infos/more-info-default.html
Normal file
@ -0,0 +1,19 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<dom-module id="more-info-default">
|
||||
<style>
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='layout vertical'>
|
||||
<template is='dom-repeat' items="[[computeDisplayAttributes(stateObj)]]" as="attribute">
|
||||
<div class='data-entry layout justified horizontal'>
|
||||
<div class='key'>[[attribute]]</div>
|
||||
<div class='value'>[[getAttributeValue(stateObj, attribute)]]</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
27
src/more-infos/more-info-default.js
Normal file
27
src/more-infos/more-info-default.js
Normal file
@ -0,0 +1,27 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
const FILTER_KEYS = ['entity_picture', 'friendly_name', 'unit_of_measurement'];
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-default',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
computeDisplayAttributes(stateObj) {
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(stateObj.attributes).filter(function(key) {
|
||||
return FILTER_KEYS.indexOf(key) === -1;
|
||||
});
|
||||
},
|
||||
|
||||
getAttributeValue(stateObj, attribute) {
|
||||
return stateObj.attributes[attribute];
|
||||
},
|
||||
});
|
22
src/more-infos/more-info-group.html
Normal file
22
src/more-infos/more-info-group.html
Normal file
@ -0,0 +1,22 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
|
||||
<dom-module id="more-info-group">
|
||||
<style>
|
||||
.child-card {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.child-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<template is='dom-repeat' items="[[states]]" as='state'>
|
||||
<div class='child-card'>
|
||||
<state-card-content state-obj="[[state]]"></state-card-content>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
43
src/more-infos/more-info-group.js
Normal file
43
src/more-infos/more-info-group.js
Normal file
@ -0,0 +1,43 @@
|
||||
import {
|
||||
entityGetters,
|
||||
moreInfoGetters
|
||||
} from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import nuclearObserver from '../util/bound-nuclear-behavior';
|
||||
|
||||
require('../cards/state-card-content');
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-group',
|
||||
|
||||
behaviors: [nuclearObserver],
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
states: {
|
||||
type: Array,
|
||||
bindNuclear: [
|
||||
moreInfoGetters.currentEntity,
|
||||
entityGetters.entityMap,
|
||||
function(currentEntity, entities) {
|
||||
// weird bug??
|
||||
if (!currentEntity) {
|
||||
return;
|
||||
}
|
||||
return currentEntity.attributes.entity_id.map(
|
||||
entities.get.bind(entities));
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
updateStates() {
|
||||
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
|
||||
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
|
||||
},
|
||||
});
|
48
src/more-infos/more-info-light.html
Normal file
48
src/more-infos/more-info-light.html
Normal file
@ -0,0 +1,48 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../../bower_components/paper-slider/paper-slider.html'>
|
||||
|
||||
<link rel='import' href='../components/ha-color-picker.html'>
|
||||
|
||||
<dom-module id='more-info-light'>
|
||||
<style>
|
||||
.brightness {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
ha-color-picker {
|
||||
display: block;
|
||||
width: 350px;
|
||||
margin: 0 auto;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.has-brightness .brightness {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.has-xy_color ha-color-picker {
|
||||
max-height: 500px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class$='[[computeClassNames(stateObj)]]'>
|
||||
<div class='brightness center horizontal layout'>
|
||||
<div>Brightness</div>
|
||||
<paper-slider
|
||||
max='255' id='brightness' value='{{brightnessSliderValue}}'
|
||||
on-change='brightnessSliderChanged' class='flex'>
|
||||
</paper-slider>
|
||||
</div>
|
||||
|
||||
<ha-color-picker on-colorselected='colorPicked' width='350' height='200'>
|
||||
</color-picker>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
63
src/more-infos/more-info-light.js
Normal file
63
src/more-infos/more-info-light.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { serviceActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import attributeClassNames from '../util/attribute-class-names';
|
||||
|
||||
require('../components/ha-color-picker');
|
||||
|
||||
const ATTRIBUTE_CLASSES = ['brightness', 'xy_color'];
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-light',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
brightnessSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged(newVal, oldVal) {
|
||||
if (newVal && newVal.state === 'on') {
|
||||
this.brightnessSliderValue = newVal.attributes.brightness;
|
||||
}
|
||||
|
||||
this.async(function() {
|
||||
this.fire('iron-resize');
|
||||
}.bind(this), 500);
|
||||
},
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
return attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
|
||||
},
|
||||
|
||||
brightnessSliderChanged(ev) {
|
||||
var bri = parseInt(ev.target.value);
|
||||
|
||||
if(isNaN(bri)) return;
|
||||
|
||||
if(bri === 0) {
|
||||
serviceActions.callTurnOff(this.stateObj.entityId);
|
||||
} else {
|
||||
serviceActions.callService('light', 'turn_on', {
|
||||
entity_id: this.stateObj.entityId,
|
||||
brightness: bri
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorPicked(ev) {
|
||||
var color = ev.detail.rgb;
|
||||
|
||||
serviceActions.callService('light', 'turn_on', {
|
||||
entity_id: this.stateObj.entityId,
|
||||
rgb_color: [color.r, color.g, color.b]
|
||||
});
|
||||
}
|
||||
|
||||
});
|
56
src/more-infos/more-info-media_player.html
Normal file
56
src/more-infos/more-info-media_player.html
Normal file
@ -0,0 +1,56 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel='import' href='../../bower_components/paper-icon-button/paper-icon-button.html'>
|
||||
|
||||
<dom-module id='more-info-media_player'>
|
||||
<style>
|
||||
.media-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
paper-icon-button[highlight] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.volume {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.has-volume_level .volume {
|
||||
max-height: 40px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class$='[[computeClassNames(stateObj)]]'>
|
||||
<div class='layout horizontal'>
|
||||
<div class='flex'>
|
||||
<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='[[!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' hidden$='[[!supportsVolumeSet]]'>
|
||||
<paper-icon-button on-tap="handleVolumeTap"
|
||||
icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button>
|
||||
<paper-slider disabled$='[[isMuted]]'
|
||||
min='0' max='100' value='[[volumeSliderValue]]'
|
||||
on-change='volumeSliderChanged' class='flex'>
|
||||
</paper-slider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
152
src/more-infos/more-info-media_player.js
Normal file
152
src/more-infos/more-info-media_player.js
Normal file
@ -0,0 +1,152 @@
|
||||
import { serviceActions } from 'home-assistant-js';
|
||||
|
||||
import Polymer from '../polymer';
|
||||
import attributeClassNames from '../util/attribute-class-names';
|
||||
|
||||
const ATTRIBUTE_CLASSES = ['volume_level'];
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-media_player',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
isOff: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isMuted: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
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(newVal) {
|
||||
if (newVal) {
|
||||
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.async(function() { this.fire('iron-resize'); }.bind(this), 500);
|
||||
},
|
||||
|
||||
computeClassNames(stateObj) {
|
||||
return attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
|
||||
},
|
||||
|
||||
computeIsOff(stateObj) {
|
||||
return stateObj.state == 'off';
|
||||
},
|
||||
|
||||
computeMuteVolumeIcon(isMuted) {
|
||||
return isMuted ? 'av:volume-off' : 'av:volume-up';
|
||||
},
|
||||
|
||||
computePlaybackControlIcon(stateObj) {
|
||||
if (this.isPlaying) {
|
||||
return this.supportsPause ? 'av:pause' : 'av:stop';
|
||||
}
|
||||
return 'av:play-arrow';
|
||||
},
|
||||
|
||||
computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff) {
|
||||
return isOff ? !supportsTurnOn : !supportsTurnOff;
|
||||
},
|
||||
|
||||
handleTogglePower() {
|
||||
this.callService(this.isOff ? 'turn_on' : 'turn_off');
|
||||
},
|
||||
|
||||
handlePrevious() {
|
||||
this.callService('media_previous_track');
|
||||
},
|
||||
|
||||
handlePlaybackControl() {
|
||||
if (this.isPlaying && !this.supportsPause) {
|
||||
alert('This case is not supported yet');
|
||||
}
|
||||
this.callService('media_play_pause');
|
||||
},
|
||||
|
||||
handleNext() {
|
||||
this.callService('media_next_track');
|
||||
},
|
||||
|
||||
handleVolumeTap() {
|
||||
if (!this.supportsVolumeMute) {
|
||||
return;
|
||||
}
|
||||
this.callService('volume_mute', { is_volume_muted: !this.isMuted });
|
||||
},
|
||||
|
||||
volumeSliderChanged(ev) {
|
||||
var volPercentage = parseFloat(ev.target.value);
|
||||
var vol = volPercentage > 0 ? volPercentage / 100 : 0;
|
||||
this.callService('volume_set', { volume_level: vol });
|
||||
},
|
||||
|
||||
callService(service, data) {
|
||||
data = data || {};
|
||||
data.entity_id = this.stateObj.entityId;
|
||||
serviceActions.callService('media_player', service, data);
|
||||
},
|
||||
});
|
12
src/more-infos/more-info-script.html
Normal file
12
src/more-infos/more-info-script.html
Normal file
@ -0,0 +1,12 @@
|
||||
<link rel='import' href='../../bower_components/polymer/polymer.html'>
|
||||
|
||||
<dom-module id='more-info-script'>
|
||||
<template>
|
||||
<div class='layout vertical'>
|
||||
<div class='data-entry layout justified horizontal'>
|
||||
<div class='key'>Last Action</div>
|
||||
<div class='value'>[[stateObj.attributes.last_action]]</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
11
src/more-infos/more-info-script.js
Normal file
11
src/more-infos/more-info-script.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Polymer from '../polymer';
|
||||
|
||||
export default Polymer({
|
||||
is: 'more-info-script',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
21
src/more-infos/more-info-sun.html
Normal file
21
src/more-infos/more-info-sun.html
Normal file
@ -0,0 +1,21 @@
|
||||
<link rel="import" href="../../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/relative-ha-datetime.html">
|
||||
|
||||
<dom-module id="more-info-sun">
|
||||
<template>
|
||||
<div class='data-entry layout justified horizontal' id='rising'>
|
||||
<div class='key'>
|
||||
Rising <relative-ha-datetime datetime-obj="[[risingDate]]"></relative-ha-datetime>
|
||||
</div>
|
||||
<div class='value'>[[risingTime]]</div>
|
||||
</div>
|
||||
|
||||
<div class='data-entry layout justified horizontal' id='setting'>
|
||||
<div class='key'>
|
||||
Setting <relative-ha-datetime datetime-obj="[[settingDate]]"></relative-ha-datetime>
|
||||
</div>
|
||||
<div class='value'>[[settingTime]]</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user