Merge pull request #132 from balloob/polymer-9

Upgrade to Polymer 1.0
This commit is contained in:
Paulus Schoutsen 2015-05-29 21:16:48 -07:00
commit 5ccb7a5856
62 changed files with 29193 additions and 2331 deletions

View File

@ -9,11 +9,11 @@
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width,
<meta name='viewport' content='width=device-width,
user-scalable=no' />
<link rel='shortcut icon' href='/static/favicon.ico' />
<link rel='icon' type='image/png'
<link rel='icon' type='image/png'
href='/static/favicon-192x192.png' sizes='192x192'>
<link rel='apple-touch-icon' sizes='192x192'
href='/static/favicon-192x192.png'>
@ -21,7 +21,7 @@
</head>
<body fullbleed>
<h3 id='init' align='center'>Initializing Home Assistant</h3>
<script src='/static/webcomponents.min.js'></script>
<script src='/static/webcomponents-lite.min.js'></script>
<link rel='import' href='/static/{{ app_url }}' />
<home-assistant auth='{{ auth }}'></home-assistant>
</body>

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "28c0680cf6ebd969dc5710c22d9c4075"
VERSION = "c164af7349b4750365f03b190e11cc8d"

File diff suppressed because one or more lines are too long

View File

@ -10,38 +10,36 @@
"ignore": [
"bower_components"
],
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#~0.6",
"font-roboto": "Polymer/font-roboto#~0.5.5",
"core-header-panel": "polymer/core-header-panel#~0.5.5",
"core-toolbar": "polymer/core-toolbar#~0.5.5",
"core-tooltip": "Polymer/core-tooltip#~0.5.5",
"core-menu": "polymer/core-menu#~0.5.5",
"core-item": "Polymer/core-item#~0.5.5",
"core-input": "Polymer/core-input#~0.5.5",
"core-icons": "polymer/core-icons#~0.5.5",
"core-image": "polymer/core-image#~0.5.5",
"core-style": "polymer/core-style#~0.5.5",
"core-label": "polymer/core-label#~0.5.5",
"paper-toast": "Polymer/paper-toast#~0.5.5",
"paper-dialog": "Polymer/paper-dialog#~0.5.5",
"paper-spinner": "Polymer/paper-spinner#~0.5.5",
"paper-button": "Polymer/paper-button#~0.5.5",
"paper-input": "Polymer/paper-input#~0.5.5",
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.5",
"paper-icon-button": "polymer/paper-icon-button#~0.5.5",
"paper-menu-button": "polymer/paper-menu-button#~0.5.5",
"paper-dropdown": "polymer/paper-dropdown#~0.5.5",
"paper-item": "polymer/paper-item#~0.5.5",
"paper-slider": "polymer/paper-slider#~0.5.5",
"paper-checkbox": "polymer/paper-checkbox#~0.5.5",
"color-picker-element": "~0.0.2",
"google-apis": "GoogleWebComponents/google-apis#~0.4.4",
"core-drawer-panel": "polymer/core-drawer-panel#~0.5.5",
"core-scroll-header-panel": "polymer/core-scroll-header-panel#~0.5.5",
"moment": "~2.10.2"
"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",
"google-apis": "GoogleWebComponents/google-apis#0.8-preview",
"moment": "^2.10.3",
"layout": "Polymer/layout",
"color-picker-element": "~0.0.3",
"paper-styles": "polymerelements/paper-styles#~1.0"
},
"resolutions": {
"webcomponentsjs": "~0.6"
"polymer": "^1.0.0",
"webcomponentsjs": "^0.7.0"
}
}

View File

@ -1,15 +1,29 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<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">
<link rel='import' href='./state-card-display.html'>
<link rel='import' href='../components/state-info.html'>
<polymer-element name="state-card-configurator" attributes="stateObj" noscript>
<dom-module id='state-card-configurator'>
<template>
<state-card-display stateObj="{{stateObj}}"></state-card-display>
<state-card-display state-obj='[[stateObj]]'></state-card-display>
<!-- pre load the image so the dialog is rendered the proper size -->
<template if="{{stateObj.attributes.description_image}}">
<img hidden src="{{stateObj.attributes.description_image}}" />
<template is='dom-if' if='[[stateObj.attributes.description_image]]'>
<img hidden src='[[stateObj.attributes.description_image]]' />
</template>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-card-configurator',
properties: {
stateObj: {
type: Object,
},
},
});
})();
</script>

View File

@ -6,43 +6,52 @@
<link rel="import" href="state-card-configurator.html">
<link rel="import" href="state-card-scene.html">
<polymer-element name="state-card-content" attributes="stateObj">
<template>
<dom-module id="state-card-content">
<style>
:host {
display: block;
}
</style>
</dom-module>
<div id='cardContainer'></div>
</template>
<script>
Polymer({
stateObjChanged: function(oldVal, newVal) {
var cardContainer = this.$.cardContainer;
(function() {
var uiUtil = window.hass.uiUtil;
if (!newVal) {
if (cardContainer.lastChild) {
cardContainer.removeChild(cardContainer.lastChild);
Polymer({
is: 'state-card-content',
properties: {
stateObj: {
type: Object,
observer: 'stateObjChanged',
}
return;
}
},
if (!oldVal || oldVal.cardType != newVal.cardType) {
if (cardContainer.lastChild) {
cardContainer.removeChild(cardContainer.lastChild);
stateObjChanged: function(newVal, oldVal) {
var root = Polymer.dom(this);
if (!newVal) {
if (root.lastChild) {
root.removeChild(root.lastChild);
}
return;
}
var stateCard = document.createElement("state-card-" + newVal.cardType);
stateCard.stateObj = newVal;
cardContainer.appendChild(stateCard);
var newCardType = uiUtil.stateCardType(newVal);
} else {
if (!oldVal || uiUtil.stateCardType(oldVal) != newCardType) {
if (root.lastChild) {
root.removeChild(root.lastChild);
}
cardContainer.lastChild.stateObj = newVal;
}
},
});
var stateCard = document.createElement("state-card-" + newCardType);
stateCard.stateObj = newVal;
root.appendChild(stateCard);
} else {
root.lastChild.stateObj = newVal;
}
},
});
})();
</script>
</polymer-element>

View File

@ -2,21 +2,35 @@
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-display" attributes="stateObj" noscript>
<template>
<style>
.state {
margin-left: 16px;
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
text-align: right;
}
</style>
<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>
<div horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
<template>
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info>
<div class='state'>[[stateObj.stateDisplay]]</div>
</div>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-card-display',
properties: {
stateObj: {
type: Object,
},
},
});
})();
</script>

View File

@ -3,25 +3,37 @@
<link rel="import" href="./state-card-display.html">
<link rel="import" href="./state-card-toggle.html">
<polymer-element name="state-card-scene" attributes="stateObj">
<dom-module id="state-card-scene">
<template>
<template if={{allowToggle}}>
<state-card-toggle stateObj="{{stateObj}}"></state-card-toggle>
<template is='dom-if' if=[[allowToggle]]>
<state-card-toggle state-obj="[[stateObj]]"></state-card-toggle>
</template>
<template if={{!allowToggle}}>
<state-card-display stateObj="{{stateObj}}"></state-card-display>
<template is='dom-if' if=[[!allowToggle]]>
<state-card-display state-obj="[[stateObj]]"></state-card-display>
</template>
</template>
<script>
</dom-module>
<script>
(function() {
Polymer({
allowToggle: false,
is: 'state-card-scene',
stateObjChanged: function(oldVal, newVal) {
this.allowToggle = newVal.state === 'off' ||
newVal.attributes.active_requested;
properties: {
stateObj: {
type: Object,
},
allowToggle: {
type: Boolean,
value: false,
computed: 'computeAllowToggle(stateObj)',
},
},
computeAllowToggle: function(stateObj) {
return stateObj.state === 'off' || stateObj.attributes.active_requested;
},
});
})();
</script>
</polymer-element>
</script>

View File

@ -2,9 +2,12 @@
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-thermostat" attributes="stateObj api">
<template>
<dom-module id="state-card-thermostat">
<style>
:host {
line-height: normal;
}
.state {
margin-left: 16px;
text-align: right;
@ -19,23 +22,36 @@
.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 horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>
<div class='target'>
{{stateObj.stateDisplay}}
</div>
<div class='current'>
Currently: {{stateObj.attributes.current_temperature}} {{stateObj.attributes.unit_of_measurement}}
<div class='current'>
<span>Currently: </span>
<span>[[stateObj.attributes.current_temperature]]</span>
<span> </span>
<span>[[stateObj.attributes.unit_of_measurement]]</span>
</div>
</div>
</div>
</div>
</template>
</template>
</dom-module>
<script>
Polymer({});
(function() {
Polymer({
is: 'state-card-thermostat',
properties: {
stateObj: {
type: Object,
},
},
});
})();
</script>
</polymer-element>

View File

@ -1,40 +1,50 @@
<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="../bower_components/core-style/core-style.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-toggle" attributes="stateObj">
<dom-module id="state-card-toggle">
<style>
paper-toggle-button {
margin-left: 16px;
}
</style>
<template>
<core-style ref='ha-paper-toggle'></core-style>
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info>
<div horizontal justified layout>
<state-info flex stateObj="{{stateObj}}"></state-info>
<paper-toggle-button self-center
checked="{{toggleChecked}}"
on-change="{{toggleChanged}}"
on-click="{{toggleClicked}}">
<paper-toggle-button class='self-center'
checked="[[toggleChecked]]"
on-change="toggleChanged"
on-click="toggleClicked">
</paper-toggle-button>
</div>
</template>
<script>
</dom-module>
<script>
(function() {
var serviceActions = window.hass.serviceActions;
Polymer({
toggleChecked: false,
is: 'state-card-toggle',
observe: {
'stateObj.state': 'stateChanged'
properties: {
stateObj: {
type: Object,
observer: 'stateObjChanged',
},
toggleChecked: {
type: Boolean,
value: false,
},
},
ready: function() {
this.forceStateChange = this.forceStateChange.bind(this);
},
toggleClicked: function(ev) {
ev.stopPropagation();
this.forceStateChange();
},
toggleChanged: function(ev) {
@ -47,22 +57,22 @@
}
},
stateObjChanged: function(oldVal, newVal) {
stateObjChanged: function(newVal) {
if (newVal) {
this.stateChanged(null, newVal.state);
this.updateToggle(newVal);
}
},
stateChanged: function(oldVal, newVal) {
this.toggleChecked = newVal === "on";
updateToggle: function(stateObj) {
this.toggleChecked = stateObj && stateObj.state === "on";
},
forceStateChange: function() {
this.stateChanged(null, this.stateObj.state);
this.updateToggle(this.stateObj);
},
turn_on: function() {
// We call stateChanged after a successful call to re-sync the toggle
// 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.
@ -70,12 +80,12 @@
},
turn_off: function() {
// We call stateChanged after a successful call to re-sync the toggle
// 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);
},
});
</script>
</polymer-element>
})();
</script>

View File

@ -2,8 +2,7 @@
<link rel="import" href="state-card-content.html">
<polymer-element name="state-card" attributes="stateObj" on-click="cardClicked">
<template>
<dom-module id="state-card">
<style>
:host {
border-radius: 2px;
@ -19,15 +18,31 @@
}
</style>
<state-card-content stateObj={{stateObj}}></state-card-content>
</template>
<script>
var uiActions = window.hass.uiActions;
<template>
<state-card-content state-obj="[[stateObj]]"></state-card-content>
</template>
</dom-module>
Polymer({
cardClicked: function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
},
});
<script>
(function(){
var uiActions = window.hass.uiActions;
Polymer({
is: 'state-card',
properties: {
stateObj: {
type: Object,
},
},
listeners: {
'click': 'cardClicked',
},
cardClicked: function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
},
});
})();
</script>
</polymer-element>

View File

@ -1,26 +1,25 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/moment-js.html">
<dom-module id="display-time">
<template>[[computeTime(dateObj)]]</template>
</dom-module>
<polymer-element name="display-time" attributes="dateObj">
<template>
{{ time }}
</template>
<script>
(function() {
var uiUtil = window.hass.uiUtil;
<script>
(function() {
var uiUtil = window.hass.uiUtil;
Polymer({
time: "",
Polymer({
is: 'display-time',
dateObjChanged: function(oldVal, newVal) {
if (newVal) {
this.time = uiUtil.formatTime(newVal);
} else {
this.time = "";
}
properties: {
dateObj: {
type: Object,
},
});
})();
</script>
</polymer-element>
},
computeTime: function(dateObj) {
return dateObj ? uiUtil.formatTime(dateObj) : '';
},
});
})();
</script>

View File

@ -1,24 +1,37 @@
<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">
<polymer-element name="domain-icon"
attributes="domain state" constructor="DomainIcon">
<dom-module id="domain-icon">
<template>
<core-icon icon="{{icon}}"></core-icon>
<iron-icon icon="[[computeIcon(domain, state)]]"></iron-icon>
</template>
<script>
Polymer({
icon: '',
</dom-module>
observe: {
'domain': 'updateIcon',
'state' : 'updateIcon',
<script>
(function() {
var uiUtil = window.hass.uiUtil;
Polymer({
is: 'domain-icon',
properties: {
domain: {
type: String,
value: '',
},
state: {
type: String,
value: '',
},
},
updateIcon: function() {
this.icon = window.hass.uiUtil.domainIcon(this.domain, this.state);
computeIcon: function(domain, state) {
return uiUtil.domainIcon(domain, state);
},
});
</script>
</polymer-element>
})();
</script>

View File

@ -1,60 +1,53 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="entity-list" attributes="cbEntityClicked">
<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>
<style>
:host {
display: block;
}
.entityContainer {
font-size: 1rem;
}
</style>
<template if={{cbEntityClicked}}>
<style>
a {
text-decoration: underline;
cursor: pointer;
}
</style>
</template>
<div>
<template repeat="{{entityID in entityIDs}}">
<div class='eventContainer'>
<a on-click={{handleClick}}>{{entityID}}</a>
</div>
<ul>
<template is='dom-repeat' items='[[entities]]' as='entity'>
<li><a href='#' on-click='entitySelected'>[[entity]]</a></li>
</template>
</div>
</ul>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
</dom-module>
Polymer(Polymer.mixin({
cbEventClicked: null,
entityIDs: [],
<script>
(function() {
Polymer({
is: 'entity-list',
attached: function() {
this.listenToStores(true);
},
behaviors: [StoreListenerBehavior],
detached: function() {
this.stopListeningToStores();
properties: {
entities: {
type: Array,
value: [],
},
},
stateStoreChanged: function(stateStore) {
this.entityIDs = stateStore.entityIDs.toArray();
this.entities = stateStore.entityIDs.toArray();
},
handleClick: function(ev) {
if(this.cbEntityClicked) {
this.cbEntityClicked(ev.path[0].innerHTML);
}
entitySelected: function(ev) {
ev.preventDefault();
this.fire('entity-selected', {entityId: ev.model.entity});
},
}, storeListenerMixIn));
</script>
</polymer-element>
});
})();
</script>

View File

@ -1,62 +1,56 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="events-list" attributes="cbEventClicked">
<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>
<style>
:host {
display: block;
}
.eventContainer {
font-size: 1rem;
}
</style>
<template if={{cbEventClicked}}>
<style>
a {
text-decoration: underline;
cursor: pointer;
}
</style>
</template>
<div>
<template repeat="{{event in events}}">
<div class='eventContainer'>
<a on-click={{handleClick}}>{{event.event}}</a>
({{event.listener_count}} listeners)
</div>
<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>
</div>
</ul>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
</dom-module>
Polymer(Polymer.mixin({
cbEventClicked: null,
events: [],
<script>
(function() {
Polymer({
is: 'events-list',
attached: function() {
this.listenToStores(true);
},
behaviors: [StoreListenerBehavior],
detached: function() {
this.stopListeningToStores();
properties: {
events: {
type: Array,
value: [],
},
},
eventStoreChanged: function(eventStore) {
this.events = eventStore.all.toArray();
},
handleClick: function(ev) {
if(this.cbEventClicked) {
this.cbEventClicked(ev.path[0].innerHTML);
}
eventSelected: function(ev) {
ev.preventDefault();
this.fire('event-selected', {eventType: ev.model.event.event});
},
}, storeListenerMixIn));
</script>
</polymer-element>
});
})();
</script>

View File

@ -2,16 +2,31 @@
<link rel="import" href="../components/logbook-entry.html">
<polymer-element name="ha-logbook" attributes="entries" noscript>
<template>
<dom-module id="ha-logbook">
<style>
.logbook {
:host {
display: block;
padding: 16px;
}
</style>
<div class='logbook'>
<template repeat="{{entries as entry}}">
<logbook-entry entryObj="{{entry}}"></logbook-entry>
<template>
<template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry>
</template>
</div>
</template>
</polymer>
</template>
</dom-module>
<script>
(function() {
Polymer({
is: 'ha-logbook',
properties: {
entries: {
type: Object,
value: [],
},
},
});
})();
</script>

View File

@ -1,37 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-toast/paper-toast.html">
<polymer-element name="ha-notifications">
<template>
<paper-toast id="toast" role="alert" text=""></paper-toast>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
lastId: null,
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
notificationStoreChanged: function(notificationStore) {
if (notificationStore.hasNewNotifications(this.lastId)) {
var toast = this.$.toast;
var notification = notificationStore.lastNotification;
if (notification) {
this.lastId = notification.id;
toast.text = notification.message;
toast.show();
}
}
},
}, storeListenerMixIn));
</script>
</polymer-element>

View File

@ -1,25 +1,27 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<polymer-element name="loading-box" attributes="text">
<dom-module id="loading-box">
<style>
.text {
display: inline-block;
line-height: 28px;
vertical-align: top;
margin-left: 8px;
}
</style>
<template>
<style>
.text {
display: inline-block;
line-height: 28px;
vertical-align: top;
margin-left: 8px;
}
</style>
<div layout='horizontal'>
<paper-spinner active="true"></paper-spinner>
<div class='text'>{{text}}</div>
<div class='text'><content></content></div>
</div>
</template>
</dom-module>
<script>
<script>
(function() {
Polymer({
text: "Loading"
is: 'loading-box',
});
</script>
</polymer-element>
})();
</script>

View File

@ -1,25 +1,25 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="domain-icon.html">
<link rel="import" href="display-time.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="logbook-entry" attributes="entryObj">
<template>
<core-style ref='ha-main'></core-style>
<dom-module id="logbook-entry">
<style>
.logbook-entry {
:host {
display: block;
line-height: 2em;
}
.time {
display-time {
width: 55px;
font-size: .8em;
color: var(--secondary-text-color);
}
.icon {
domain-icon {
margin: 0 8px 0 16px;
color: var(--primary-text-color);
}
.name {
@ -27,29 +27,38 @@
}
.message {
color: var(--primary-text-color);
}
a {
color: var(--accent-color);
}
</style>
<div horizontal layout class='logbook-entry'>
<display-time dateObj="{{entryObj.when}}" class='time secondary-text-color'></display-time>
<domain-icon domain="{{entryObj.domain}}" class='icon primary-text-color'></domain-icon>
<div class='message primary-text-color' flex>
<template if="{{!entryObj.entityId}}">
<span class='name'>{{entryObj.name}}</span>
</template>
<template if="{{entryObj.entityId}}">
<a href='#' on-click="{{entityClicked}}" class='name'>{{entryObj.name}}</a>
</template>
{{entryObj.message}}
<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>
</div>
</template>
</template>
</dom-module>
<script>
(function() {
var uiActions = window.hass.uiActions;
Polymer({
is: 'logbook-entry',
entityClicked: function(ev) {
ev.preventDefault();
uiActions.showMoreInfoDialog(this.entryObj.entityId);
@ -58,4 +67,3 @@
})();
</script>
</polymer-element>

View File

@ -1,50 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="./loading-box.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="recent-states" attributes="stateObj">
<template>
<core-style ref='ha-data-table'></core-style>
<template if="{{recentStates === null}}">
<loading-box text="Loading recent states"></loading-box>
</template>
<template if="{{recentStates !== null}}">
<div layout vertical>
<template repeat="{{recentStates as state}}">
<div layout justified horizontal class='data-entry'>
<div>
{{state.state}}
</div>
<div class='data'>
<relative-ha-datetime datetime="{{stateObj.last_changed}}">
</relative-ha-datetime>
</div>
</div>
</template>
<template if="{{recentStates.length == 0}}">
There are no recent states.
</template>
</div>
</template>
</template>
<script>
Polymer({
recentStates: null,
stateObjChanged: function() {
this.recentStates = null;
window.hass.callApi(
'GET', 'history/entity/' + this.stateObj.entityId + '/recent_states').then(
function(states) {
this.recentStates = states.slice(1);
}.bind(this));
},
});
</script>
</polymer-element>

View File

@ -2,49 +2,73 @@
<link rel="import" href="../resources/moment-js.html">
<polymer-element name="relative-ha-datetime" attributes="datetime datetimeObj">
<dom-module id="relative-ha-datetime">
<template>
{{ relativeTime }}
<span>[[relativeTime]]</span>
</template>
<script>
(function() {
var UPDATE_INTERVAL = 60000; // 60 seconds
</dom-module>
var parseDateTime = window.hass.util.parseDateTime;
<script>
(function() {
var UPDATE_INTERVAL = 60000; // 60 seconds
Polymer({
relativeTime: "",
parsedDateTime: null,
var parseDateTime = window.hass.util.parseDateTime;
created: function() {
this.updateRelative = this.updateRelative.bind(this);
Polymer({
is: 'relative-ha-datetime',
properties: {
datetime: {
type: String,
observer: 'datetimeChanged',
},
attached: function() {
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
datetimeObj: {
type: Object,
observer: 'datetimeObjChanged',
},
detached: function() {
clearInterval(this._interval);
parsedDateTime: {
type: Object,
},
datetimeChanged: function(oldVal, newVal) {
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
this.updateRelative();
relativeTime: {
type: String,
value: 'not set',
},
},
datetimeObjChanged: function(oldVal, newVal) {
this.parsedDateTime = newVal;
relativeTime: "",
parsedDateTime: null,
this.updateRelative();
},
created: function() {
this.updateRelative = this.updateRelative.bind(this);
},
updateRelative: function() {
this.relativeTime = this.parsedDateTime ?
moment(this.parsedDateTime).fromNow() : "";
},
});
})();
</script>
</polymer-element>
attached: function() {
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
},
detached: function() {
clearInterval(this._interval);
},
datetimeChanged: function(newVal) {
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
this.updateRelative();
},
datetimeObjChanged: function(newVal) {
this.parsedDateTime = newVal;
this.updateRelative();
},
updateRelative: function() {
this.relativeTime = this.parsedDateTime ?
moment(this.parsedDateTime).fromNow() : "";
},
});
})();
</script>

View File

@ -1,72 +1,58 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-menu/core-menu.html">
<link rel="import" href="../bower_components/core-menu/core-submenu.html">
<link rel="import" href="../bower_components/core-item/core-item.html">
<link rel="import" href="../bower_components/paper-menu/paper-menu.html">
<link rel="import" href="domain-icon.html">
<polymer-element name="services-list" attributes="cbServiceClicked">
<template>
<style>
:host {
display: block;
<dom-module id="services-list">
<style>
ul {
margin: 0;
padding: 0;
}
core-menu {
margin-top: 0;
font-size: 1rem;
li {
list-style: none;
line-height: 2em;
}
a {
display: block;
color: var(--accent-color);
}
</style>
</style>
<template if={{cbServiceClicked}}>
<style>
a, core-submenu {
text-decoration: underline;
cursor: pointer;
}
</style>
</template>
<div>
<core-menu selected="0">
<template repeat="{{domain in domains}}">
<core-submenu icon="{{domain | getIcon}}" label="{{domain}}">
<template repeat="{{service in domain | getServices}}">
<a on-click={{serviceClicked}} data-domain={{domain}}>{{service}}</a>
</template>
</core-submenu>
</template>
</core-menu>
</div>
<template>
<ul>
<template is='dom-repeat' items="[[domains]]" as="domain">
<template is='dom-repeat' items="[[computeServices(domain)]]" as="service">
<li><a href='#' on-click='serviceClicked'>
<span>[[domain]]</span>/<span>[[service]]</span>
</a></li>
</template>
</template>
</ul>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
</dom-module>
Polymer(Polymer.mixin({
domains: [],
services: null,
cbServiceClicked: null,
<script>
(function() {
Polymer({
is: 'services-list',
attached: function() {
this.listenToStores(true);
behaviors: [StoreListenerBehavior],
properties: {
domains: {
type: Array,
value: [],
},
services: {
type: Object,
},
},
detached: function() {
this.stopListeningToStores();
},
getIcon: function(domain) {
return hass.uiUtil.domainIcon(domain);
},
getServices: function(domain) {
computeServices: function(domain) {
return this.services.get(domain).toArray();
},
@ -76,15 +62,10 @@
},
serviceClicked: function(ev) {
if(this.cbServiceClicked) {
var target = ev.path[0];
var domain = target.getAttributeNode("data-domain").value;
var service = target.innerHTML;
this.cbServiceClicked(domain, service);
}
}
}, storeListenerMixIn));
</script>
</polymer-element>
ev.preventDefault();
this.fire(
'service-selected', {domain: ev.model.domain, service: ev.model.service});
},
});
})();
</script>

View File

@ -1,105 +1,105 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-image/core-image.html">
<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">
<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>
<polymer-element name="state-badge" attributes="stateObj">
<template>
<style>
:host {
position: relative;
display: inline-block;
width: 45px;
background-color: #4fc3f7;
color: white;
border-radius: 50%;
transition: all .3s ease-in-out;
}
div {
height: 45px;
text-align: center;
}
core-image {
border-radius: 50%;
}
domain-icon {
margin: 0 auto;
}
/* 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>
<div horizontal layout center>
<domain-icon id="icon"
domain="{{stateObj.domain}}" data-domain="{{stateObj.domain}}"
state="{{stateObj.state}}" data-state="{{stateObj.state}}">
<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 if="{{stateObj.attributes.entity_picture}}">
<core-image
sizing="cover" fit
src="{{stateObj.attributes.entity_picture}}"></core-image>
<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>
<script>
Polymer({
observe: {
'stateObj.state': 'updateIconColor',
'stateObj.attributes.brightness': 'updateIconColor',
'stateObj.attributes.xy_color[0]': 'updateIconColor',
'stateObj.attributes.xy_color[1]': 'updateIconColor'
</dom-module>
<script>
Polymer({
is: 'state-badge',
properties: {
stateObj: {
type: Object,
observer: 'updateIconColor',
},
},
/**
* Called when an attribute changes that influences the color of the icon.
*/
updateIconColor: function(oldVal, newVal) {
var state = this.stateObj;
/**
* Called when an attribute changes that influences the color of the icon.
*/
updateIconColor: function(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) {
// for domain light, set color of icon to light color if available
if(state.domain == "light" && state.state == "on" &&
state.attributes.brightness && state.attributes.xy_color) {
var rgb = this.xyBriToRgb(state.attributes.xy_color[0],
state.attributes.xy_color[1],
state.attributes.brightness);
this.$.icon.style.color = "rgb(" + rgb.map(Math.floor).join(",") + ")";
} else {
this.$.icon.style.color = null;
}
},
// from http://stackoverflow.com/questions/22894498/philips-hue-convert-xy-from-api-to-hex-or-rgb
xyBriToRgb: function (x, y, bri) {
z = 1.0 - x - y;
Y = bri / 255.0; // Brightness of lamp
X = (Y / y) * x;
Z = (Y / y) * z;
r = X * 1.612 - Y * 0.203 - Z * 0.302;
g = -X * 0.509 + Y * 1.412 + Z * 0.066;
b = X * 0.026 - Y * 0.072 + Z * 0.962;
r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055;
g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055;
b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055;
maxValue = Math.max(r,g,b);
r /= maxValue;
g /= maxValue;
b /= maxValue;
r = r * 255; if (r < 0) { r = 255 };
g = g * 255; if (g < 0) { g = 255 };
b = b * 255; if (b < 0) { b = 255 };
return [r, g, b]
var rgb = this.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;
}
},
});
</script>
</polymer-element>
// from http://stackoverflow.com/questions/22894498/philips-hue-convert-xy-from-api-to-hex-or-rgb
xyBriToRgb: function (x, y, bri) {
z = 1.0 - x - y;
Y = bri / 255.0; // Brightness of lamp
X = (Y / y) * x;
Z = (Y / y) * z;
r = X * 1.612 - Y * 0.203 - Z * 0.302;
g = -X * 0.509 + Y * 1.412 + Z * 0.066;
b = X * 0.026 - Y * 0.072 + Z * 0.962;
r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055;
g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055;
b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055;
maxValue = Math.max(r,g,b);
r /= maxValue;
g /= maxValue;
b /= maxValue;
r = r * 255; if (r < 0) { r = 255; }
g = g * 255; if (g < 0) { g = 255; }
b = b * 255; if (b < 0) { b = 255; }
return [r, g, b];
}
});
</script>

View File

@ -2,50 +2,49 @@
<link rel="import" href="../cards/state-card.html">
<polymer-element name="state-cards" attributes="states" noscript>
<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: 16px;
margin: 16px auto;
}
</style>
<template>
<style>
:host {
display: block;
width: 100%;
}
<div class='horizontal layout wrap'>
@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: 16px;
margin: 16px auto;
}
</style>
<div horizontal layout wrap>
<template repeat="{{states as state}}">
<state-card class="state-card" stateObj={{state}}></state-card>
<template is='dom-repeat' items="{{states}}">
<state-card class="state-card" state-obj="[[item]]"></state-card>
</template>
<template if="{{states.length == 0}}">
<template if="[[computeEmptyStates(states)]]">
<div class='no-states-content'>
<content></content>
</div>
@ -53,4 +52,23 @@
</div>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-cards',
properties: {
states: {
type: Array,
value: [],
},
},
computeEmptyStates: function(states) {
return states.length === 0;
},
});
})();
</script>

View File

@ -0,0 +1,196 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<script>
(function() {
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 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: function() {
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++) {
var currentDate = new Date(prevDate);
// 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);
},
});
})();
</script>

View File

@ -0,0 +1,115 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<script>
(function() {
Polymer({
is: 'state-history-chart-timeline',
properties: {
data: {
type: Object,
observer: 'dataChanged',
},
isAttached: {
type: Boolean,
value: false,
observer: 'dataChanged',
},
},
created: function() {
this.style.display = 'block';
},
attached: function() {
this.isAttached = true;
},
dataChanged: function() {
this.drawChart();
},
drawChart: function() {
if (!this.isAttached) {
return;
}
var root = Polymer.dom(this);
var stateHistory = this.data;
while (root.lastChild) {
root.removeChild(root.lastChild);
}
if (!stateHistory || stateHistory.length === 0) {
return;
}
// debugger;
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]);
};
// people can pass in history of 1 entityId or a collection.
// var stateHistory;
// if (_.isArray(data[0])) {
// stateHistory = data;
// } else {
// stateHistory = [data];
// isSingleDevice = true;
// }
var 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, new Date());
numTimelines++;
}.bind(this));
chart.draw(dataTable, {
height: 55 + numTimelines * 42,
// interactive properties require CSS, the JS api puts it on the document
// instead of inside our Shadow DOM.
enableInteractivity: false,
timeline: {
showRowLabels: stateHistory.length > 1
},
hAxis: {
format: 'H:mm'
},
});
},
});
})();
</script>

View File

@ -0,0 +1,145 @@
<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;
}
</style>
<template>
<google-legacy-loader on-api-load="googleApiLoaded"></google-legacy-loader>
<div hidden$="{{!isLoading}}" class='loading-container'>
<loading-box>Loading history data</loading-box>
</div>
<template is='dom-if' if='[[!isLoading]]'>
<template is='dom-if' if='[[groupedStateHistory.timeline]]'>
<state-history-chart-timeline data='[[groupedStateHistory.timeline]]'
is-single-device='[[isSingleDevice]]'>
</state-history-chart-timeline>
</template>
<template is='dom-if' if='[[groupedStateHistory.line]]'>
<template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<state-history-chart-line unit='[[extractUnit(item)]]'
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'>
</state-history-chart-line>
</template>
</template>
</template>
</template>
</dom-module>
<script>
(function() {
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(stateHistory)',
},
isSingleDevice: {
type: Boolean,
computed: 'computeIsSingleDevice(stateHistory)',
},
},
computeIsSingleDevice: function(stateHistory) {
return stateHistory && stateHistory.length == 1;
},
computeGroupedStateHistory: function(stateHistory) {
var lineChartDevices = {};
var timelineDevices = [];
if (!stateHistory) {
return {line: unitStates, timeline: timelineDevices};
}
stateHistory.forEach(function(stateInfo) {
if (!stateInfo || stateInfo.length === 0) {
return;
}
var unit;
for (var i = 0; i < stateInfo.length && !unit; i++) {
unit = stateInfo[i].attributes.unit_of_measurement;
}
if (unit) {
if (!(unit in lineChartDevices)) {
lineChartDevices[unit] = [stateInfo];
} else {
lineChartDevices[unit].push(stateInfo);
}
} else {
timelineDevices.push(stateInfo);
}
});
timelineDevices = timelineDevices.length > 0 && timelineDevices;
var unitStates = Object.keys(lineChartDevices).map(function(unit) {
return [unit, lineChartDevices[unit]]; });
return {line: unitStates, timeline: timelineDevices};
},
googleApiLoaded: function() {
google.load("visualization", "1", {
packages: ["timeline", "corechart"],
callback: function() {
this.apiLoaded = true;
}.bind(this)
});
},
computeIsLoading: function(isLoadingData, apiLoaded) {
return isLoadingData || !apiLoaded;
},
extractUnit: function(arr) {
return arr[0];
},
extractData: function(arr) {
return arr[1];
},
});
})();
</script>

View File

@ -1,48 +1,69 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<!-- <link rel="import" href="../bower_components/core-tooltip/core-tooltip.html"> -->
<link rel="import" href="state-badge.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="state-info" attributes="stateObj" noscript>
<dom-module id="state-info">
<style>
:host {
line-height: normal;
}
state-badge {
float: left;
}
.name {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.info {
margin-left: 60px;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
font-size: 1rem;
}
</style>
<template>
<style>
state-badge {
float: left;
}
.name {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.info {
margin-left: 60px;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
}
</style>
<div>
<state-badge stateObj="{{stateObj}}"></state-badge>
<state-badge state-obj='[[stateObj]]'></state-badge>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class='name'>[[stateObj.entityDisplay]]</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.lastChangedAsDate | formatDateTime}}" position="bottom">
<relative-ha-datetime datetimeObj="{{stateObj.lastChangedAsDate}}"></relative-ha-datetime>
</core-tooltip>
<div class='time-ago'>
<!-- <core-tooltip label="[[computeTooltipLabel(stateObj)]]" position="bottom"> -->
<relative-ha-datetime datetime-obj='[[stateObj.lastChangedAsDate]]'></relative-ha-datetime>
<!-- </core-tooltip> -->
</div>
</div>
</div>
</template>
</polymer-element>
</dom-module>
<script>
(function() {
Polymer({
is: 'state-info',
properties: {
stateObj: {
type: Object,
},
},
computeTooltipLabel: function(stateObj) {
// stateObj.lastChangedAsDate | formatDateTime
return 'Label TODO';
},
});
})();
</script>

View File

@ -1,321 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/google-apis/google-jsapi.html">
<polymer-element name="state-timeline" attributes="stateHistory isLoadingData">
<template>
<style>
:host {
display: block;
}
#loadingbox {
text-align: center;
}
.loadingmessage {
margin-top: 10px;
}
.singlelinechart {
min-height:140px;
}
</style>
<div style='width: 100%; height: auto;' hidden?="{{!isLoading}}" >
<div layout horizontal center id="splash">
<div layout vertical center flex>
<div id="loadingbox">
<paper-spinner active="true"></paper-spinner><br />
<div class="loadingmessage">{{spinnerMessage}}</div>
</div>
</div>
</div>
</div>
<google-jsapi on-api-load="{{googleApiLoaded}}"></google-jsapi>
<div id="timeline" style='width: 100%; height: auto;' class="{{ {singlelinechart: isSingleDevice && hasLineChart } | tokenList}}" hidden?="{{isLoadingData}}"></div>
<div id="line_graphs" style='width: 100%; height: auto;' hidden?="{{isLoadingData}}"></div>
</template>
<script>
Polymer({
apiLoaded: false,
stateHistory: null,
isLoading: true,
isLoadingData: false,
spinnerMessage: "Loading history data...",
isSingleDevice: false,
hasLineChart: false,
googleApiLoaded: function() {
google.load("visualization", "1", {
packages: ["timeline", "corechart"],
callback: function() {
this.apiLoaded = true;
this.drawChart();
}.bind(this)
});
},
stateHistoryChanged: function() {
this.drawChart();
},
isLoadingDataChanged: function() {
if(this.isLoadingData) {
isLoading = true;
}
},
drawChart: function() {
if (!this.apiLoaded || !this.stateHistory) {
return;
}
this.isLoading = true;
var container = this.$.timeline;
var chart = new google.visualization.Timeline(container);
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]);
};
if (this.stateHistory.length === 0) {
return;
}
this.hasLineChart = false;
this.isSingleDevice = false;
// people can pass in history of 1 entityId or a collection.
var stateHistory;
if (_.isArray(this.stateHistory[0])) {
stateHistory = this.stateHistory;
} else {
stateHistory = [this.stateHistory];
this.isSingleDevice = true;
}
var lineChartDevices = {};
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;
//get the latest update to get the graph type from the component attributes
var attributes = stateInfo[stateInfo.length - 1].attributes;
//if the device has a unit of meaurment it will be added as a line graph further down
if(attributes.unit_of_measurement) {
if(!lineChartDevices[attributes.unit_of_measurement]){
lineChartDevices[attributes.unit_of_measurement] = [];
}
lineChartDevices[attributes.unit_of_measurement].push(stateInfo);
this.hasLineChart = true;
return;
}
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, new Date());
numTimelines++;
}.bind(this));
chart.draw(dataTable, {
height: 55 + numTimelines * 42,
// interactive properties require CSS, the JS api puts it on the document
// instead of inside our Shadow DOM.
enableInteractivity: false,
timeline: {
showRowLabels: stateHistory.length > 1
},
hAxis: {
format: 'H:mm'
},
});
/**************************************************
The following code gererates line line graphs for devices with continuous
values(which are devices that have a unit_of_measurment 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.
**************************************************/
while (this.$.line_graphs.firstChild) {
this.$.line_graphs.removeChild(this.$.line_graphs.firstChild);
}
for (var key in lineChartDevices) {
var deviceStates = lineChartDevices[key];
if(this.isSingleDevice) {
container = this.$.timeline;
}
else {
container = document.createElement("DIV");
this.$.line_graphs.appendChild(container);
}
var chart = new google.visualization.LineChart(container);
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: key}
},
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(var i = 0; i < times.length; i++) {
var currentDate = new Date(prevDate);
// 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 entires 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);
}
this.isLoading = (!this.isLoadingData) ? false : true;
},
});
</script>
</polymer-element>

View File

@ -1,11 +1,11 @@
<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/core-style/core-style.html">
<link rel="import" href="../bower_components/core-icons/notification-icons.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">
<polymer-element name="stream-status">
<template>
<link rel="import" href="../bower_components/iron-icons/notification-icons.html">
<dom-module id="stream-status">
<style>
:host {
display: inline-block;
@ -16,29 +16,31 @@
vertical-align: middle;
}
</style>
<core-style ref='ha-paper-toggle'></core-style>
<template>
<iron-icon icon="warning" hidden$="{{!hasError}}"></iron-icon>
<paper-toggle-button id="toggle" on-change='toggleChanged' hidden$="{{hasError}}"></paper-toggle-button>
</template>
</dom-module>
<core-icon icon="warning" hidden?="{{!hasError}}"></core-icon>
<paper-toggle-button id="toggle" on-change={{toggleChanged}} hidden?="{{hasError}}"></paper-toggle-button>
</template>
<script>
var streamActions = window.hass.streamActions;
var authStore = window.hass.authStore;
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
isStreaming: false,
hasError: false,
Polymer({
is: 'stream-status',
icon: "swap-vert-circle",
color: 'red',
behaviors: [StoreListenerBehavior],
attached: function() {
this.listenToStores(true);
},
properties: {
isStreaming: {
type: Boolean,
value: false,
},
detached: function() {
this.stopListeningToStores();
hasError: {
type: Boolean,
value: false,
},
},
streamStoreChanged: function(streamStore) {
@ -53,6 +55,5 @@
streamActions.start(authStore.authToken);
}
},
}, storeListenerMixIn));
});
</script>
</polymer-element>

View File

@ -1,18 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../bower_components/paper-dialog/paper-dialog-transition.html">
<polymer-element name="ha-dialog" extends="paper-dialog">
<template>
<core-style ref='ha-dialog'></core-style>
<shadow></shadow>
</template>
<script>
Polymer({
layered: true,
backdrop: true,
transition: 'core-transition-bottom',
});
</script>
</polymer-element>

View File

@ -1,120 +1,152 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="ha-dialog.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-timeline.html">
<link rel="import" href="../components/state-history-charts.html">
<link rel="import" href="../more-infos/more-info-content.html">
<polymer-element name="more-info-dialog">
<template>
<ha-dialog id="dialog" on-core-overlay-open="{{dialogOpenChanged}}">
<div>
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
</state-card-content>
<template if="{{hasHistoryComponent}}">
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingHistoryData}}"></state-timeline>
</template>
<more-info-content
stateObj="{{stateObj}}"
dialogOpen="{{dialogOpen}}"></more-info-content>
</div>
</ha-dialog>
</template>
<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>
<h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2>
<div>
<template is='dom-if' if="[[hasHistoryComponent]]">
<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>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var stateStore = window.hass.stateStore;
var stateHistoryStore = window.hass.stateHistoryStore;
var stateHistoryActions = window.hass.stateHistoryActions;
(function() {
var stateStore = window.hass.stateStore;
var stateHistoryStore = window.hass.stateHistoryStore;
var stateHistoryActions = window.hass.stateHistoryActions;
Polymer(Polymer.mixin({
entityId: false,
stateObj: null,
stateHistory: null,
hasHistoryComponent: false,
dialogOpen: false,
isLoadingHistoryData: false,
Polymer({
is: 'more-info-dialog',
observe: {
'stateObj.attributes': 'reposition'
},
behaviors: [StoreListenerBehavior],
created: function() {
this.dialogOpenChanged = this.dialogOpenChanged.bind(this);
},
properties: {
entityId: {
type: String,
},
attached: function() {
this.listenToStores(true);
},
stateObj: {
type: Object,
},
detached: function() {
this.stopListeningToStores();
},
stateHistory: {
type: Object,
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
isLoadingHistoryData: {
type: Boolean,
value: false,
},
stateStoreChanged: function() {
var newState = this.entityId ? stateStore.get(this.entityId) : null;
hasHistoryComponent: {
type: Boolean,
value: false,
},
if (newState !== this.stateObj) {
this.stateObj = newState;
}
},
dialogOpen: {
type: Boolean,
value: false,
},
},
stateHistoryStoreChanged: function() {
var newHistory;
listeners: {
'iron-overlay-opened': 'onIronOverlayOpened',
'iron-overlay-closed': 'onIronOverlayClosed'
},
if (this.hasHistoryComponent && this.entityId) {
newHistory = stateHistoryStore.get(this.entityId);
} else {
newHistory = null;
}
this.isLoadingHistoryData = false;
if (newHistory !== this.stateHistory) {
this.stateHistory = newHistory;
}
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
dialogOpenChanged: function(ev) {
// we get CustomEvent, undefined and true/false from polymer…
if (typeof ev === 'object') {
this.dialogOpen = ev.detail;
}
},
stateStoreChanged: function() {
var newState = this.entityId ? stateStore.get(this.entityId) : null;
changeEntityId: function(entityId) {
this.entityId = entityId;
if (newState !== this.stateObj) {
this.stateObj = newState;
}
},
this.stateStoreChanged();
this.stateHistoryStoreChanged();
stateHistoryStoreChanged: function() {
var newHistory;
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
this.isLoadingHistoryData = true;
stateHistoryActions.fetch(entityId);
}
},
if (this.hasHistoryComponent && this.entityId) {
newHistory = [stateHistoryStore.get(this.entityId)];
} else {
newHistory = null;
}
/**
* Whenever the attributes change, the more info component can
* hide or show elements. We will reposition the dialog.
*/
reposition: function(oldVal, newVal) {
// Only resize if already open
if(this.$.dialog.opened) {
this.job('resizeAfterLayoutChange', function() {
this.$.dialog.resizeHandler();
}.bind(this), 1000);
}
},
this.isLoadingHistoryData = false;
show: function(entityId) {
this.changeEntityId(entityId);
if (newHistory !== this.stateHistory) {
this.stateHistory = newHistory;
}
},
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
}, storeListenerMixIn));
onIronOverlayOpened: function() {
this.dialogOpen = true;
},
onIronOverlayClosed: function() {
this.dialogOpen = false;
},
changeEntityId: function(entityId) {
this.entityId = entityId;
this.stateStoreChanged();
this.stateHistoryStoreChanged();
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
this.isLoadingHistoryData = true;
stateHistoryActions.fetch(entityId);
}
},
show: function(entityId) {
this.changeEntityId(entityId);
this.debounce('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
});
})();
</script>
</polymer-element>

@ -1 +1 @@
Subproject commit 69ee1c49af12caf00655c66d56474b5c1bcac1c1
Subproject commit 015edf9c28a63122aa8f6bc153f0c0ddfaad1caa

View File

@ -1,41 +1,62 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/font-roboto/roboto.html">
<link rel='import' href='bower_components/polymer/polymer.html'>
<link rel="import" href="resources/home-assistant-style.html">
<link rel="import" href="resources/home-assistant-js.html">
<link rel='import' href='bower_components/paper-styles/typography.html'>
<link rel="import" href="layouts/login-form.html">
<link rel="import" href="layouts/home-assistant-main.html">
<link rel='import' href='resources/home-assistant-js.html'>
<link rel='import' href='resources/home-assistant-icons.html'>
<link rel='import' href='resources/store-listener-behavior.html'>
<link rel='import' href='layouts/login-form.html'>
<link rel='import' href='layouts/home-assistant-main.html'>
<link rel='import' href='resources/home-assistant-style.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>
<home-assistant-icons></home-assistant-icons>
<polymer-element name="home-assistant" attributes="auth">
<template>
<style>
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
font-weight: 300;
}
</style>
<home-assistant-api auth="{{auth}}"></home-assistant-api>
<template if="{{!loaded}}">
<template is='dom-if' if='[[!loaded]]'>
<login-form></login-form>
</template>
<template if="{{loaded}}">
<template is='dom-if' if='[[loaded]]'>
<home-assistant-main></home-assistant-main>
</template>
</template>
<script>
</dom-module>
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn,
uiActions = window.hass.uiActions,
preferenceStore = window.hass.preferenceStore;
Polymer(Polymer.mixin({
loaded: false,
Polymer({
is: 'home-assistant',
hostAttributes: {
auth: null,
},
behaviors: [StoreListenerBehavior],
properties: {
loaded: {
type: Boolean,
value: false,
},
},
ready: function() {
// remove the HTML init message
@ -43,23 +64,16 @@
// if auth was given, tell the backend
if(this.auth) {
uiActions.validateAuth(this.auth, false);
uiActions.validateAuth(this.auth, false);
} else if (preferenceStore.hasAuthToken) {
uiActions.validateAuth(preferenceStore.authToken, false);
uiActions.validateAuth(preferenceStore.authToken, false);
}
},
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
syncStoreChanged: function(syncStore) {
this.loaded = syncStore.initialLoadDone;
},
}, storeListenerMixIn));
</script>
</polymer-element>
});
})();
</script>

View File

@ -0,0 +1,11 @@
{
"removeComments": true,
"removeCommentsFromCDATA": true,
"removeCDATASectionsFromCDATA": true,
"collapseWhitespace": true,
"collapseBooleanAttributes": true,
"removeScriptTypeAttributes": true,
"removeStyleLinkTypeAttributes": true,
"minifyJS": true,
"minifyCSS": true
}

View File

@ -1,64 +1,50 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/layout/layout.html'>
<link rel="import" href="../bower_components/core-drawer-panel/core-drawer-panel.html">
<link rel="import" href="../bower_components/core-header-panel/core-header-panel.html">
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="../bower_components/core-menu/core-menu.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/paper-item/paper-item.html">
<link rel='import' href='../bower_components/paper-drawer-panel/paper-drawer-panel.html'>
<link rel='import' href='../bower_components/paper-header-panel/paper-header-panel.html'>
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
<link rel='import' href='../bower_components/paper-menu/paper-menu.html'>
<link rel='import' href='../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../bower_components/paper-item/paper-icon-item.html'>
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel="import" href="../layouts/partial-states.html">
<link rel="import" href="../layouts/partial-history.html">
<link rel="import" href="../layouts/partial-logbook.html">
<link rel="import" href="../layouts/partial-dev-fire-event.html">
<link rel="import" href="../layouts/partial-dev-call-service.html">
<link rel="import" href="../layouts/partial-dev-set-state.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="../components/ha-notifications.html">
<link rel="import" href="../components/ha-modals.html">
<link rel="import" href="../components/stream-status.html">
<link rel='import' href='../managers/notification-manager.html'>
<link rel='import' href='../managers/modal-manager.html'>
<polymer-element name="home-assistant-main">
<template>
<core-style ref="ha-headers"></core-style>
<link rel='import' href='../components/stream-status.html'>
<dom-module id='home-assistant-main'>
<style>
.sidenav {
background: #fafafa;
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
color: #757575;
overflow: hidden;
}
core-toolbar {
font-weight: normal;
padding-left: 24px;
.sidenav paper-menu {
--paper-menu-color: var(--secondary-text-color);
--paper-menu-background-color: #fafafa;
}
.sidenav-menu {
overflow: auto;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
paper-icon-item {
cursor: pointer;
}
.sidenav-menu core-icon {
margin-right: 24px;
}
.sidenav-menu > paper-item {
min-height: 53px;
.divider {
border-top: 1px solid #e0e0e0;
}
.text {
padding: 16px;
border-top: 1px solid #e0e0e0;
}
.label {
font-size: 14px;
}
@ -67,192 +53,288 @@
}
</style>
<ha-notifications></ha-notifications>
<ha-modals></ha-modals>
<template>
<notification-manager></notification-manager>
<modal-manager></modal-manager>
<core-drawer-panel id="drawer" on-core-responsive-change="{{responsiveChanged}}">
<core-header-panel mode="scroll" drawer class='sidenav'>
<core-toolbar>
Home Assistant
</core-toolbar>
<core-menu id="menu" class="sidenav-menu"
selected="0" excludedLocalNames="div" on-core-select="{{menuSelect}}"
layout vertical>
<paper-item data-panel="states">
<core-icon icon="apps"></core-icon>
States
</paper-item>
<paper-drawer-panel id='drawer' narrow='{{narrow}}'>
<paper-header-panel mode='scroll' drawer class='sidenav fit'>
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div title>Home Assistant</div>
</paper-toolbar>
<template repeat="{{activeFilters as filter}}">
<paper-item data-panel="states_{{filter}}">
<core-icon icon="{{filter | filterIcon}}"></core-icon>
{{filter | filterName}}
<paper-menu id='menu' class='layout vertical fit'
selectable='[data-panel]' attr-for-selected='data-panel'
on-iron-select='menuSelect' selected='[[selected]]'>
<paper-icon-item data-panel='states'>
<iron-icon item-icon icon='apps'></iron-icon> States
</paper-icon-item>
<template is='dom-repeat' items='{{activeFilters}}'>
<paper-icon-item data-panel$='[[filterType(item)]]'>
<iron-icon item-icon icon='[[filterIcon(item)]]'></iron-icon>
<span>[[filterName(item)]]</span>
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasHistoryComponent]]'>
<paper-icon-item data-panel='history'>
<iron-icon item-icon icon='assessment'></iron-icon>
History
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasLogbookComponent]]'>
<paper-icon-item data-panel='logbook'>
<iron-icon item-icon icon='list'></iron-icon>
Logbook
</paper-icon-item>
</template>
<div class='flex'></div>
<paper-icon-item data-panel='logout'>
<iron-icon item-icon icon='exit-to-app'></iron-icon>
Log Out
</paper-icon-item>
<paper-item class='divider horizontal layout justified'>
<div>Streaming updates</div>
<stream-status></stream-status>
</paper-item>
</template>
<template if="{{hasHistoryComponent}}">
<paper-item data-panel="history">
<core-icon icon="assessment"></core-icon>
History
</paper-item>
</template>
<div class='text label divider'>Developer Tools</div>
<div class='dev-tools layout horizontal justified'>
<paper-icon-button
icon='settings-remote' data-panel$='[[selectedDevService]]'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-ethernet' data-panel$='[[selectedDevState]]'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-input-antenna' data-panel$='[[selectedDevEvent]]'
on-click='handleDevClick'></paper-icon-button>
</div>
</paper-menu>
</paper-header-panel>
<template if="{{hasLogbookComponent}}">
<paper-item data-panel="logbook">
<core-icon icon="list"></core-icon>
Logbook
</paper-item>
</template>
<template is='dom-if' if='[[!hideStates]]'>
<partial-states
main narrow='[[narrow]]'
filter='[[stateFilter]]'>
</partial-states>
</template>
<div flex></div>
<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>
<paper-item on-click="{{handleLogOutClick}}">
<core-icon icon="exit-to-app"></core-icon>
Log Out
</paper-item>
</template>
</dom-module>
<div class='text' horizontal layout center>
<div flex>Streaming updates</div>
<stream-status></stream-status>
</div>
<div class='text label'>Developer Tools</div>
<div class='dev-tools' layout horizontal justified>
<paper-icon-button
icon="settings-remote" data-panel='call-service'
on-click="{{handleDevClick}}"></paper-icon-button>
<paper-icon-button
icon="settings-ethernet" data-panel='set-state'
on-click="{{handleDevClick}}"></paper-icon-button>
<paper-icon-button
icon="settings-input-antenna" data-panel='fire-event'
on-click="{{handleDevClick}}"></paper-icon-button>
</div>
</core-menu>
</core-header-panel>
<!--
This is the main partial, never remove it from the DOM but hide it
to speed up when people click on states.
-->
<partial-states hidden?="{{hideStates}}"
main narrow="{{narrow}}"
togglePanel="{{togglePanel}}"
filter="{{stateFilter}}">
</partial-states>
<template if="{{selected == 'history'}}">
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
</template>
<template if="{{selected == 'logbook'}}">
<partial-logbook main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-logbook>
</template>
<template if="{{selected == 'fire-event'}}">
<partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event>
</template>
<template if="{{selected == 'set-state'}}">
<partial-dev-set-state main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-set-state>
</template>
<template if="{{selected == 'call-service'}}">
<partial-dev-call-service main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-call-service>
</template>
</core-drawer-panel>
</template>
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn;
var authActions = window.hass.authActions;
var uiUtil = window.hass.uiUtil;
var uiConstants = window.hass.uiConstants;
var authActions = window.hass.authActions;
Polymer(Polymer.mixin({
selected: "states",
stateFilter: null,
narrow: false,
activeFilters: [],
hasHistoryComponent: false,
hasLogbookComponent: false,
var uiUtil = window.hass.uiUtil;
var uiConstants = window.hass.uiConstants;
isStreaming: false,
hasStreamError: false,
Polymer({
is: 'home-assistant-main',
hideStates: false,
behaviors: [StoreListenerBehavior],
attached: function() {
this.togglePanel = this.togglePanel.bind(this);
properties: {
selected: {
type: String,
value: 'states',
},
this.listenToStores(true);
},
stateFilter: {
type: String,
value: null,
},
detached: function() {
this.stopListeningToStores();
},
narrow: {
type: Boolean,
},
stateStoreChanged: function(stateStore) {
this.activeFilters = stateStore.domains.filter(function(domain) {
return domain in uiConstants.STATE_FILTERS;
}).toArray();
},
activeFilters: {
type: Array,
value: [],
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasLogbookComponent = componentStore.isLoaded('logbook');
},
hasHistoryComponent: {
type: Boolean,
value: false,
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
this.hasStreamError = streamStore.hasError;
},
hasLogbookComponent: {
type: Boolean,
value: false,
},
menuSelect: function(ev, detail, sender) {
if (detail.isSelected) {
this.selectPanel(detail.item);
}
},
isStreaming: {
type: Boolean,
value: false,
},
handleDevClick: function(ev, detail, sender) {
this.$.menu.selected = -1;
this.selectPanel(ev.target);
},
hasStreamError: {
type: Boolean,
value: false,
},
selectPanel: function(element) {
var newChoice = element.dataset.panel;
hideStates: {
type: Boolean,
value: false,
},
if(newChoice !== this.selected) {
this.togglePanel();
selectedHistory: {
type: String,
value: 'history',
readOnly: true,
},
isSelectedHistory: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedHistory)',
},
selectedLogbook: {
type: String,
value: 'logbook',
readOnly: true,
},
isSelectedLogbook: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedLogbook)',
},
selectedDevEvent: {
type: String,
value: 'devEvent',
readOnly: true,
},
isSelectedDevEvent: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevEvent)',
},
selectedDevState: {
type: String,
value: 'devState',
readOnly: true,
},
isSelectedDevState: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevState)',
},
selectedDevService: {
type: String,
value: 'devService',
readOnly: true,
},
isSelectedDevService: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevService)',
},
},
listeners: {
'menu.core-select': 'menuSelect',
'open-menu': 'openDrawer',
},
stateStoreChanged: function(stateStore) {
this.activeFilters = stateStore.domains.filter(function(domain) {
return domain in uiConstants.STATE_FILTERS;
}).toArray();
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasLogbookComponent = componentStore.isLoaded('logbook');
},
menuSelect: function(ev, detail, sender) {
this.selectPanel(this.$.menu.selected);
},
handleDevClick: function(ev, detail, sender) {
// prevent it from highlighting first menu item
document.activeElement.blur();
this.selectPanel(ev.target.parentElement.dataset.panel);
},
selectPanel: function(newChoice) {
if (newChoice == 'logout') {
this.handleLogOut();
return;
} else if(newChoice == this.selected) {
return;
}
this.closeDrawer();
this.selected = newChoice;
if (newChoice.substr(0, 7) === 'states_') {
this.hideStates = false;
this.stateFilter = newChoice.substr(7);
} else {
this.hideStates = newChoice !== 'states';
this.stateFilter = null;
}
},
openDrawer: function() {
this.$.drawer.openDrawer();
},
closeDrawer: function() {
this.$.drawer.closeDrawer();
},
handleLogOut: function() {
authActions.logOut();
},
computeIsSelected: function(selected, selectedType) {
return selected === selectedType;
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return uiConstants.STATE_FILTERS[filter];
},
filterType: function(filter) {
return 'states_' + filter;
}
if (this.selected.substr(0, 7) === 'states_') {
this.hideStates = false;
this.stateFilter = this.selected.substr(7);
} else {
this.hideStates = this.selected !== 'states';
this.stateFilter = null;
}
},
responsiveChanged: function(ev, detail, sender) {
this.narrow = detail.narrow;
},
togglePanel: function() {
this.$.drawer.togglePanel();
},
handleLogOutClick: function() {
authActions.logOut();
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return uiConstants.STATE_FILTERS[filter];
},
}, storeListenerMixIn));
});
})();
</script>
</polymer-element>
</script>

View File

@ -1,79 +1,79 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/layout/layout.html">
<link rel="import" href="../bower_components/core-label/core-label.html">
<!-- WIP <link rel="import" href="../bower_components/core-label/core-label.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-decorator.html">
<link rel="import" href="../bower_components/core-input/core-input.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">
<polymer-element name="login-form">
<link rel="import" href="../resources/store-listener-behavior.html">
<dom-module id="login-form">
<style>
#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>
<style>
#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>
<div layout horizontal center fit class='login' id="splash">
<div layout vertical center flex>
<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' layout vertical>
<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 id='loginform' hidden?="{{isValidating || isLoggedIn}}">
<paper-input-decorator label="Password" id="passwordDecorator">
<input is="core-input" type="password" id="passwordInput"
value="{{authToken}}" on-keyup="{{passwordKeyup}}">
</paper-input-decorator>
<div horizontal center layout>
<core-label horizontal layout>
<paper-checkbox for checked={{rememberLogin}}></paper-checkbox>
Remember
</core-label>
<paper-button on-click={{validatePassword}}>Log In</paper-button>
<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 || isLoggedIn)}}">
<div id="validatebox" hidden$="[[!isValidating]]">
<paper-spinner active="true"></paper-spinner><br />
<div class="validatemessage">{{spinnerMessage}}</div>
<div class="validatemessage">Loading data…</div>
</div>
</div>
</div>
@ -81,43 +81,53 @@
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
</dom-module>
<script>
(function() {
var uiActions = window.hass.uiActions;
Polymer(Polymer.mixin({
MSG_VALIDATING: "Validating password…",
MSG_LOADING_DATA: "Loading data…",
Polymer({
is: 'login-form',
authToken: "",
rememberLogin: false,
behaviors: [StoreListenerBehavior],
isValidating: false,
isLoggedIn: false,
properties: {
isValidating: {
type: Boolean,
value: false,
},
spinnerMessage: "",
isInvalid: {
type: Boolean,
value: false,
},
errorMessage: {
type: String,
value: '',
}
},
listeners: {
'passwordInput.keydown': 'passwordKeyDown',
'loginButton.click': 'validatePassword',
},
attached: function() {
this.focusPassword();
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
authStoreChanged: function(authStore) {
this.isValidating = authStore.isValidating;
this.isLoggedIn = authStore.isLoggedIn;
this.spinnerMessage = this.isValidating ? this.MSG_VALIDATING : this.MSG_LOADING_DATA;
if (authStore.lastAttemptInvalid) {
this.$.passwordDecorator.error = authStore.lastAttemptMessage;
this.$.passwordDecorator.isInvalid = true;
this.errorMessage = authStore.lastAttemptMessage;
this.isInvalid = true;
}
if (!(this.isValidating && this.isLoggedIn)) {
this.job('focusPasswordBox', this.focusPassword.bind(this));
if (!this.isValidating) {
setTimeout(this.focusPassword.bind(this), 0);
}
},
@ -125,23 +135,25 @@
this.$.passwordInput.focus();
},
passwordKeyup: function(ev) {
passwordKeyDown: function(ev) {
// validate on enter
if(ev.keyCode === 13) {
this.validatePassword();
ev.preventDefault();
// clear error after we start typing again
} else if(this.$.passwordDecorator.isInvalid) {
this.$.passwordDecorator.isInvalid = false;
} else if(this.isInvalid) {
this.isInvalid = false;
}
},
validatePassword: function() {
this.$.hideKeyboardOnFocus.focus();
uiActions.validateAuth(this.authToken, this.rememberLogin);
uiActions.validateAuth(this.$.passwordInput.value, this.$.rememberLogin.checked);
},
}, storeListenerMixIn));
</script>
</polymer-element>
});
})();
</script>

View File

@ -1,28 +1,41 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel="import" href="../bower_components/core-scroll-header-panel/core-scroll-header-panel.html">
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel='import' href='../bower_components/paper-scroll-header-panel/paper-scroll-header-panel.html'>
<link rel="import" href="../bower_components/core-style/core-style.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'>
<polymer-element name="partial-base" attributes="narrow togglePanel" noscript>
<template>
<core-style ref="ha-headers"></core-style>
<dom-module id='partial-base'>
<template>
<paper-scroll-header-panel class='fit'>
<paper-toolbar>
<paper-icon-button icon='menu' hidden$='[[!narrow]]' on-click='toggleMenu'></paper-icon-button>
<div title>
<content select='[header-title]'></content>
</div>
<content select='[header-buttons]'></content>
</paper-toolbar>
<core-scroll-header-panel fit fixed="{{!narrow}}">
<core-toolbar>
<paper-icon-button
id="navicon" icon="menu" hidden?="{{!narrow}}"
on-click="{{togglePanel}}"></paper-icon-button>
<div flex>
<content select="[header-title]"></content>
</div>
<content select="[header-buttons]"></content>
</core-toolbar>
<content></content>
</paper-scroll-header-panel>
</template>
</dom-module>
<content></content>
<script>
(function() {
Polymer({
is: 'partial-base',
</core-scroll-header-panel>
</template>
</polymer>
properties: {
narrow: {
type: Boolean,
value: false,
},
},
toggleMenu: function() {
this.fire('open-menu');
},
});
})();
</script>

View File

@ -2,87 +2,100 @@
<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-input-decorator.html">
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.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">
<polymer-element name="partial-dev-call-service" attributes="narrow togglePanel">
<template>
<dom-module id="partial-dev-call-service">
<style>
.form {
padding: 24px;
background-color: white;
}
.form {
padding: 24px;
background-color: white;
}
.ha-form {
margin-right: 16px;
}
</style>
<template>
<partial-base narrow="[[narrow]]">
<span header-title>Call Service</span>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Call Service</span>
<div class='form fit'>
<p>
Call a service from a component.
</p>
<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 layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
<div class='ha-form' flex?="{{!narrow}}">
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
<paper-input-decorator
label="Service Data (JSON, optional)"
floatingLabel="true">
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
</paper-input-decorator>
<paper-button on-click={{clickCallService}}>Call Service</paper-button>
</div>
<div class='sidebar'>
<b>Available services:</b>
<services-list cbServiceClicked={{serviceSelected}}></services-list>
<div>
<h4>Available services:</h4>
<services-list on-service-selected='serviceSelected'></services-list>
</div>
</div>
</div>
</div>
</partial-base>
</template>
</dom-module>
</partial-base>
</template>
<script>
var serviceActions = window.hass.serviceActions;
(function() {
var serviceActions = window.hass.serviceActions;
Polymer({
ready: function() {
// to ensure callback methods work..
this.serviceSelected = this.serviceSelected.bind(this);
},
Polymer({
is: 'partial-dev-call-service',
setService: function(domain, service) {
this.$.inputDomain.value = domain;
this.$.inputService.value = service;
},
properties: {
narrow: {
type: Boolean,
value: false,
},
serviceSelected: function(domain, service) {
this.setService(domain, service);
},
domain: {
type: String,
value: '',
},
clickCallService: function() {
try {
serviceActions.callService(
this.$.inputDomain.value,
this.$.inputService.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
service: {
type: String,
value: '',
},
});
serviceData: {
type: String,
value: '',
},
},
serviceSelected: function(ev) {
this.domain = ev.detail.domain;
this.service = ev.detail.service;
},
callService: function() {
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: function(narrow) {
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
},
});
})();
</script>
</polymer-element>

View File

@ -2,79 +2,90 @@
<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-input-decorator.html">
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.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">
<polymer-element name="partial-dev-fire-event" attributes="narrow togglePanel">
<template>
<dom-module id="partial-dev-fire-event">
<style>
.form {
padding: 24px;
background-color: white;
}
.form {
padding: 24px;
background-color: white;
}
.ha-form {
margin-right: 16px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Fire Event</span>
<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='form fit'>
<p>
Fire an event on the event bus.
</p>
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
<div class='ha-form' flex?="{{!narrow}}">
<paper-input
id="inputType" label="Event Type" floatingLabel="true"
autofocus required></paper-input>
<paper-input-decorator
label="Event Data (JSON, optional)"
floatingLabel="true">
<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>
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
</paper-input-decorator>
<paper-button on-click={{clickFireEvent}}>Fire Event</paper-button>
</div>
<div class='sidebar'>
<b>Available events:</b>
<events-list cbEventClicked={{eventSelected}}></event-list>
<div>
<h4>Available events:</h4>
<events-list on-event-selected='eventSelected'></event-list>
</div>
</div>
</div>
</div>
</partial-base>
</partial-base>
</template>
</dom-module>
</template>
<script>
var eventActions = window.hass.eventActions;
(function() {
var eventActions = window.hass.eventActions;
Polymer({
ready: function() {
// to ensure callback methods work..
this.eventSelected = this.eventSelected.bind(this);
},
Polymer({
is: 'partial-dev-fire-event',
eventSelected: function(eventType) {
this.$.inputType.value = eventType;
},
properties: {
eventType: {
type: String,
value: '',
},
clickFireEvent: function() {
try {
eventActions.fire(
this.$.inputType.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
});
eventData: {
type: String,
value: '',
},
},
eventSelected: function(ev) {
this.eventType = ev.detail.eventType;
},
fireEvent: function() {
var eventData;
try {
eventData = this.eventData ? JSON.parse(this.eventData) : {};
} catch (err) {
alert("Error parsing JSON: " + err);
return;
}
eventActions.fire(this.eventType, eventData);
},
computeFormClasses: function(narrow) {
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
},
});
})();
</script>
</polymer-element>

View File

@ -2,105 +2,109 @@
<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-input-decorator.html">
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.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">
<polymer-element name="partial-dev-set-state" attributes="narrow togglePanel">
<template>
<dom-module id="partial-dev-set-state">
<style>
.form {
padding: 24px;
background-color: white;
}
.form {
padding: 24px;
background-color: white;
}
.ha-form {
margin-right: 16px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Set State</span>
<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 layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
<div class='ha-form' flex?="{{!narrow}}">
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
<paper-input-decorator
label="State attributes (JSON, optional)"
floatingLabel="true">
<paper-autogrow-textarea id="inputDataWrapper">
<textarea id="inputData"></textarea>
</paper-autogrow-textarea>
</paper-input-decorator>
<paper-button on-click={{clickSetState}}>Set State</paper-button>
<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='sidebar'>
<b>Current entities:</b>
<entity-list cbEntityClicked={{entitySelected}}></entity-list>
<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>
</div>
</partial-base>
</template>
<script>
var stateStore = window.hass.stateStore;
var stateActions = window.hass.stateActions;
(function() {
var stateStore = window.hass.stateStore;
var stateActions = window.hass.stateActions;
Polymer({
ready: function() {
// to ensure callback methods work..
this.entitySelected = this.entitySelected.bind(this);
},
Polymer({
is: 'partial-dev-set-state',
setEntityId: function(entityId) {
this.$.inputEntityID.value = entityId;
},
properties: {
entityId: {
type: String,
value: '',
},
setState: function(state) {
this.$.inputState.value = state;
},
state: {
type: String,
value: '',
},
setStateData: function(stateData) {
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
stateAttributes: {
type: String,
value: '',
},
},
this.$.inputData.value = value;
setStateData: function(stateData) {
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
// not according to the spec but it works...
this.$.inputDataWrapper.update(this.$.inputData);
},
this.$.inputData.value = value;
entitySelected: function(entityId) {
this.setEntityId(entityId);
// not according to the spec but it works...
this.$.inputDataWrapper.update(this.$.inputData);
},
var state = stateStore.get(entityId);
this.setState(state.state);
this.setStateData(state.attributes);
},
entitySelected: function(ev) {
var state = stateStore.get(ev.detail.entityId);
clickSetState: function(ev) {
try {
stateActions.set(
this.$.inputEntityID.value,
this.$.inputState.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}
);
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
});
this.entityId = state.entityId;
this.state = state.state;
this.stateAttributes = JSON.stringify(state.attributes, null, ' ');
},
handleSetState: function() {
var attr;
try {
attr = this.stateAttributes ? JSON.parse(this.stateAttributes) : {};
} catch (err) {
alert("Error parsing JSON: " + err);
return;
}
stateActions.set(this.entityId, this.state, attr);
},
computeFormClasses: function(narrow) {
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
},
});
})();
</script>
</polymer-element>

View File

@ -1,13 +1,11 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.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-timeline.html">
<link rel="import" href="../components/state-history-charts.html">
<polymer-element name="partial-history" attributes="narrow togglePanel">
<template>
<dom-module id="partial-history">
<style>
.content {
background-color: white;
@ -17,33 +15,42 @@
padding: 8px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>History</span>
<template>
<partial-base narrow="[[narrow]]">
<span header-title>History</span>
<span header-buttons>
<paper-icon-button icon="refresh"
on-click="{{handleRefreshClick}}"></paper-icon-button>
</span>
<paper-icon-button icon="refresh" header-buttons
on-click="handleRefreshClick"></paper-icon-button>
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingData}}"></state-timeline>
</div>
</partial-base>
</template>
<div class$="[[computeContentClasses(narrow)]]">
<state-history-charts state-history="[[stateHistory]]"
is-loading-data="[[isLoadingData]]"></state-history-charts>
</div>
</partial-base>
</template>
</dom-module>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
(function() {
var stateHistoryActions = window.hass.stateHistoryActions;
Polymer(Polymer.mixin({
stateHistory: null,
isLoadingData: false,
Polymer({
is: 'partial-history',
attached: function() {
this.listenToStores(true);
},
behaviors: [StoreListenerBehavior],
detached: function() {
this.stopListeningToStores();
properties: {
narrow: {
type: Boolean,
},
stateHistory: {
type: Object,
},
isLoadingData: {
type: Boolean,
value: false,
},
},
stateHistoryStoreChanged: function(stateHistoryStore) {
@ -62,6 +69,10 @@
this.isLoadingData = true;
stateHistoryActions.fetchAll();
},
}, storeListenerMixIn));
</script>
</polymer>
computeContentClasses: function(narrow) {
return 'flex content ' + (narrow ? 'narrow' : 'wide');
},
});
})();
</script>

View File

@ -1,43 +1,50 @@
<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/ha-logbook.html">
<polymer-element name="partial-logbook" attributes="narrow togglePanel">
<template>
<dom-module id="partial-logbook">
<style>
.content {
background-color: white;
padding: 8px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Logbook</span>
<template>
<partial-base narrow="[[narrow]]">
<span header-title>Logbook</span>
<span header-buttons>
<paper-icon-button icon="refresh"
on-click="{{handleRefreshClick}}"></paper-icon-button>
</span>
<paper-icon-button icon="refresh" header-buttons
on-click="handleRefresh"></paper-icon-button>
<ha-logbook entries="[[entries]]"></ha-logbook>
</partial-base>
</template>
</dom-module>
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
<ha-logbook entries="{{entries}}"></ha-logbook>
</div>
</partial-base>
</template>
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn;
var logbookActions = window.hass.logbookActions;
Polymer(Polymer.mixin({
entries: null,
Polymer({
is: 'partial-logbook',
attached: function() {
this.listenToStores(true);
},
behaviors: [StoreListenerBehavior],
detached: function() {
this.stopListeningToStores();
properties: {
narrow: {
type: Boolean,
value: false,
},
entries: {
type: Array,
value: [],
},
},
logbookStoreChanged: function(logbookStore) {
@ -48,9 +55,9 @@
this.entries = logbookStore.all.toArray();
},
handleRefreshClick: function() {
handleRefresh: function() {
logbookActions.fetch();
},
}, storeListenerMixIn));
});
})();
</script>
</polymer>

View File

@ -1,18 +1,21 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-cards.html">
<polymer-element name="partial-states" attributes="narrow togglePanel filter">
<template>
<core-style ref="ha-animations"></core-style>
<dom-module id="partial-states">
<style>
.listening {
.content-wrapper {
position: relative;
height: 100%;
background-color: #E5E5E5;
}
.content-wrapper ::content .listening {
position: absolute;
top: 0;
left: 0;
@ -36,65 +39,124 @@
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>{{headerTitle}}</span>
<template>
<partial-base narrow="[[narrow]]">
<span header-title>{{headerTitle}}</span>
<span header-buttons>
<paper-icon-button icon="refresh" class="{{isFetching && 'ha-spin'}}"
on-click="{{handleRefreshClick}}" hidden?="{{isStreaming}}"></paper-icon-button>
<paper-icon-button icon="{{isListening ? 'av:mic-off' : 'av:mic' }}" hidden?={{!canListen}}
on-click="{{handleListenClick}}"></paper-icon-button>
</span>
<span header-buttons>
<paper-icon-button icon="refresh" class$="[[computeRefreshButtonClass(isFetching)]]"
on-click="handleRefresh" hidden$="[[isStreaming]]"></paper-icon-button>
<paper-icon-button icon="[[listenButtonIcon]]" hidden$={{!canListen}}
on-click="handleListenClick"></paper-icon-button>
</span>
<div class='listening' hidden?="{{!isListening && !isTransmitting}}" on-click={{handleListenClick}}>
<core-icon icon="av:hearing"></core-icon> {{finalTranscript}}
<span class='interimTranscript'>{{interimTranscript}}</span>
<paper-spinner active?="{{isTransmitting}}"></paper-spinner>
</div>
<div class='content-wrapper'>
<div class='listening' hidden$="[[!showListenInterface]]"
on-click="handleListenClick">
<iron-icon icon="av:hearing"></iron-icon> <span>{{finalTranscript}}</span>
<span class='interimTranscript'>[[interimTranscript]]</span>
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
</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>
<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>
</partial-base>
</template>
<script>
(function(){
var storeListenerMixIn = window.hass.storeListenerMixIn;
var syncActions = window.hass.syncActions;
var voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var uiConstants = window.hass.uiConstants;
Polymer(Polymer.mixin({
headerTitle: "States",
states: [],
isFetching: false,
isStreaming: false,
Polymer({
is: 'partial-states',
canListen: false,
voiceSupported: false,
hasConversationComponent: false,
isListening: false,
isTransmittingVoice: false,
interimTranscript: '',
finalTranscript: '',
behaviors: [StoreListenerBehavior],
ready: function() {
this.voiceSupported = voiceActions.isSupported();
},
properties: {
/**
* Title to show in the header
*/
headerTitle: {
type: String,
value: 'States',
},
attached: function() {
this.listenToStores(true);
},
/**
* If header is to be shown in narrow mode.
*/
narrow: {
type: Boolean,
value: false,
},
detached: function() {
this.stopListeningToStores();
filter: {
type: String,
value: null,
observer: 'filterChanged',
},
voiceSupported: {
type: Boolean,
value: voiceActions.isSupported(),
},
isFetching: {
type: Boolean,
value: false,
},
isStreaming: {
type: Boolean,
value: false,
},
canListen: {
type: Boolean,
value: false,
},
isListening: {
type: Boolean,
value: false,
},
isTransmitting: {
type: Boolean,
value: false,
},
interimTranscript: {
type: String,
value: '',
},
finalTranscript: {
type: String,
value: '',
},
listenButtonIcon: {
type: String,
computed: 'computeListenButtonIcon(isListening)'
},
showListenInterface: {
type: Boolean,
computed: 'computeShowListenInterface(isListening,isTransmitting)'
}
},
componentStoreChanged: function(componentStore) {
@ -143,12 +205,12 @@
return !(state.domain in uiConstants.STATE_FILTERS);
});
}
this.states = states.toArray().filter(
function (el) {return !el.attributes.hidden});
function (el) {return !el.attributes.hidden;});
},
handleRefreshClick: function() {
handleRefresh: function() {
syncActions.fetchAll();
},
@ -159,7 +221,20 @@
voiceActions.listen();
}
},
}, storeListenerMixIn));
computeListenButtonIcon: function(isListening) {
return isListening ? 'av:mic-off' : 'av:mic';
},
computeShowListenInterface: function(isListening,isTransmitting) {
return isListening || isTransmitting;
},
computeRefreshButtonClass: function(isFetching) {
if (isFetching) {
return 'ha-spin';
}
},
});
})();
</script>
</polymer>

View File

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

View File

@ -0,0 +1,47 @@
<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>
<script>
(function() {
Polymer({
is: 'notification-manager',
behaviors: [StoreListenerBehavior],
properties: {
text: {
type: String,
value: '',
},
lastId: {
type: Number,
},
},
notificationStoreChanged: function(notificationStore) {
if (notificationStore.hasNewNotifications(this.lastId)) {
var notification = notificationStore.lastNotification;
this.lastId = notification.id;
this.text = notification.message;
this.$.toast.show();
}
},
});
})();
</script>

View File

@ -1,9 +1,9 @@
<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-spinner/paper-spinner.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/paper-button/paper-button.html'>
<polymer-element name="more-info-configurator" attributes="stateObj">
<template>
<link rel='import' href='../components/loading-box.html'>
<dom-module id='more-info-configurator'>
<style>
p {
margin: 8px 0;
@ -25,62 +25,78 @@
text-align: center;
height: 41px;
}
p.submit paper-spinner {
margin-right: 16px;
}
p.submit span {
display: inline-block;
vertical-align: top;
margin-top: 6px;
}
</style>
<template>
<div class='layout vertical'>
<template is='dom-if' if='[[isConfigurable]]'>
<div layout vertical>
<template if="{{stateObj.state == 'configure'}}">
<p hidden$='[[!stateObj.attributes.description]]'>[[stateObj.attributes.description]]</p>
<p hidden?="{{!stateObj.attributes.description}}">
{{stateObj.attributes.description}}
</p>
<p class='error' hidden$='[[!stateObj.attributes.errors]]'>[[stateObj.attributes.errors]]</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='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>
<p class='submit'>
<paper-button raised on-click="{{submitClicked}}"
hidden?="{{action !== 'display'}}">
{{stateObj.attributes.submit_caption || "Set configuration"}}
</paper-button>
<loading-box hidden$='[[!isConfiguring]]'>Configuring</loading-box>
</p>
<span hidden?="{{action !== 'configuring'}}">
<paper-spinner active="true"></paper-spinner><span>Configuring…</span>
</span>
</p>
</template>
</div>
</template>
</dom-module>
</template>
</div>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
(function() {
var syncActions = window.hass.syncActions;
var serviceActions = window.hass.serviceActions;
Polymer(Polymer.mixin({
action: "display",
isStreaming: false,
Polymer({
is: 'more-info-configurator',
attached: function() {
this.listenToStores(true);
behaviors: [StoreListenerBehavior],
properties: {
stateObj: {
type: Object,
},
action: {
type: String,
value: 'display',
},
isStreaming: {
type: Boolean,
value: false,
},
isConfigurable: {
type: Boolean,
computed: 'computeIsConfigurable(stateObj)',
},
isConfiguring: {
type: Boolean,
value: false,
},
submitCaption: {
type: String,
computed: 'computeSubmitCaption(stateObj)',
},
},
detached: function() {
this.stopListeningToStores();
computeIsConfigurable: function(stateObj) {
return stateObj.state == 'configure';
},
computeSubmitCaption: function(stateObj) {
return stateObj.attributes.submit_caption || 'Set configuration';
},
streamStoreChanged: function(streamStore) {
@ -88,15 +104,15 @@
},
submitClicked: function() {
this.action = "configuring";
this.isConfiguring = true;
var data = {
configure_id: this.stateObj.attributes.configure_id
};
serviceActions.callService('configurator', 'configure', data).then(
function() {
this.action = 'display';
this.isConfiguring = false;
if (!this.isStreaming) {
syncActions.fetchAll();
@ -104,9 +120,9 @@
}.bind(this),
function() {
this.action = 'display';
this.isConfiguring = false;
}.bind(this));
},
}, storeListenerMixIn));
});
})();
</script>
</polymer-element>

View File

@ -1,73 +1,77 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel="import" href="more-info-default.html">
<link rel="import" href="more-info-light.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-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'>
<polymer-element name="more-info-content" attributes="stateObj dialogOpen">
<template>
<dom-module id='more-info-content'>
<style>
:host {
display: block;
}
</style>
<div id='moreInfoContainer' class='{{classNames}}'></div>
</template>
</dom-module>
<script>
Polymer({
classNames: '',
dialogOpen: false,
(function() {
var uiUtil = window.hass.uiUtil;
observe: {
'stateObj.attributes': 'stateAttributesChanged',
},
Polymer({
is: 'more-info-content',
dialogOpenChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
properties: {
stateObj: {
type: Object,
observer: 'stateObjChanged',
},
if (moreInfoContainer.lastChild) {
moreInfoContainer.lastChild.dialogOpen = newVal;
}
},
dialogOpen: {
type: Boolean,
value: false,
},
},
stateObjChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
dialogOpenChanged: function(newVal, oldVal) {
var root = Polymer.dom(this);
if (!newVal) {
if (moreInfoContainer.lastChild) {
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
if (root.lastChild) {
root.lastChild.dialogOpen = newVal;
}
return;
}
},
if (!oldVal || oldVal.moreInfoType != newVal.moreInfoType) {
if (moreInfoContainer.lastChild) {
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
stateObjChanged: function(newVal, oldVal) {
var root = Polymer.dom(this);
var newMoreInfoType;
if (!newVal || !(newMoreInfoType = uiUtil.stateMoreInfoType(newVal))) {
if (root.lastChild) {
root.removeChild(root.lastChild);
}
return;
}
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
moreInfo.stateObj = newVal;
moreInfo.dialogOpen = this.dialogOpen;
moreInfoContainer.appendChild(moreInfo);
if (!oldVal || uiUtil.stateMoreInfoType(oldVal) != newMoreInfoType) {
} else {
if (root.lastChild) {
root.removeChild(root.lastChild);
}
moreInfoContainer.lastChild.dialogOpen = this.dialogOpen;
moreInfoContainer.lastChild.stateObj = newVal;
var moreInfo = document.createElement('more-info-' + newMoreInfoType);
moreInfo.stateObj = newVal;
moreInfo.dialogOpen = this.dialogOpen;
root.appendChild(moreInfo);
}
},
} else {
stateAttributesChanged: function(oldVal, newVal) {
if (!newVal) return;
root.lastChild.dialogOpen = this.dialogOpen;
root.lastChild.stateObj = newVal;
this.classNames = Object.keys(newVal).map(
function(key) { return "has-" + key; }).join(' ');
},
});
}
},
});
})();
</script>
</polymer-element>

View File

@ -1,34 +1,41 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<polymer-element name="more-info-default" attributes="stateObj">
<template>
<core-style ref='ha-key-value-table'></core-style>
<dom-module id="more-info-default">
<style>
.data-entry .value {
max-width: 200px;
}
</style>
<div layout vertical>
<template repeat="{{key in stateObj.attributes | getKeys}}">
<div layout justified horizontal class='data-entry'>
<div class='key'>
{{key}}
<template>
<div class='layout vertical'>
<template is='dom-repeat' items="[[getAttributes(stateObj)]]" as="attribute">
<div class='data-entry layout justified horizontal'>
<div class='key'>[[attribute]]</div>
<div class='value'>[[getAttributeValue(stateObj, attribute)]]</div>
</div>
<div class='value'>
{{stateObj.attributes[key]}}
</div>
</div>
</template>
</div>
</template>
</template>
</div>
</template>
</dom-module>
<script>
(function() {
Polymer({
getKeys: function(obj) {
return Object.keys(obj || {});
}
is: 'more-info-default',
properties: {
stateObj: {
type: Object,
},
},
getAttributes: function(stateObj) {
return stateObj ? Object.keys(stateObj.attributes) : [];
},
getAttributeValue: function(stateObj, attribute) {
return stateObj.attributes[attribute];
},
});
})();
</script>
</polymer-element>

View File

@ -2,8 +2,7 @@
<link rel="import" href="../cards/state-card-content.html">
<polymer-element name="more-info-group" attributes="stateObj">
<template>
<dom-module id="more-info-group">
<style>
.child-card {
margin-bottom: 8px;
@ -13,37 +12,44 @@
margin-bottom: 0;
}
</style>
<template repeat="{{states as state}}">
<state-card-content stateObj="{{state}}" class='child-card'>
</state-card-content>
<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>
</template>
</dom-module>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var stateStore = window.hass.stateStore;
(function() {
var stateStore = window.hass.stateStore;
Polymer(Polymer.mixin({
attached: function() {
this.listenToStores(true);
},
Polymer({
is: 'more-info-group',
detached: function() {
this.stopListeningToStores();
},
behaviors: [StoreListenerBehavior],
stateStoreChanged: function() {
this.updateStates();
},
properties: {
stateObj: {
type: Object,
observer: 'updateStates',
},
stateObjChanged: function() {
this.updateStates();
},
states: {
type: Array,
value: [],
},
},
updateStates: function() {
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
},
}, storeListenerMixIn));
stateStoreChanged: function() {
this.updateStates();
},
updateStates: function() {
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
},
});
})();
</script>
</polymer-element>

View File

@ -1,10 +1,9 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-slider/paper-slider.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/paper-slider/paper-slider.html'>
<link rel="import" href="../bower_components/color-picker-element/dist/color-picker.html">
<link rel='import' href='../bower_components/color-picker-element/dist/color-picker.html'>
<polymer-element name="more-info-light" attributes="stateObj">
<template>
<dom-module id='more-info-light'>
<style>
.brightness {
margin-bottom: 8px;
@ -26,81 +25,92 @@
max-height: 0px;
overflow: hidden;
transition: max-height .5s ease-in .3s;
transition: max-height .5s ease-in;
}
:host-context(.has-brightness) .brightness {
.has-brightness .brightness {
max-height: 500px;
}
:host-context(.has-xy_color) color-picker {
.has-xy_color color-picker {
max-height: 500px;
}
</style>
<div>
<div class='brightness'>
<div center horizontal layout>
<template>
<div class$='[[computeClassNames(stateObj)]]'>
<div class='brightness center horizontal layout'>
<div>Brightness</div>
<paper-slider
max="255" flex id='brightness' value='{{brightnessSliderValue}}'
on-change="{{brightnessSliderChanged}}">
max='255' id='brightness' value='{{brightnessSliderValue}}'
on-change='brightnessSliderChanged' class='flex'>
</paper-slider>
</div>
<color-picker on-colorselected='colorPicked' width='350' height='200'>
</color-picker>
</div>
</template>
</dom-module>
<color-picker id="colorpicker" width="350" height="200">
</color-picker>
</div>
</template>
<script>
var serviceActions = window.hass.serviceActions;
(function() {
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['brightness', 'xy_color'];
Polymer({
brightnessSliderValue: 0,
Polymer({
is: 'more-info-light',
observe: {
'stateObj.attributes.brightness': 'stateObjBrightnessChanged',
},
properties: {
stateObj: {
type: Object,
observer: 'stateObjChanged',
},
stateObjChanged: function(oldVal, newVal) {
if (newVal && newVal.state === 'on') {
this.brightnessSliderValue = newVal.attributes.brightness;
}
},
brightnessSliderValue: {
type: Number,
value: 0,
}
},
stateObjBrightnessChanged: function(oldVal, newVal) {
this.brightnessSliderValue = newVal;
},
stateObjChanged: function(newVal, oldVal) {
if (newVal && newVal.state === 'on') {
this.brightnessSliderValue = newVal.attributes.brightness;
}
domReady: function() {
this.$.colorpicker.addEventListener('colorselected', this.colorPicked.bind(this));
},
this.debounce('more-info-light-animation-finish', function() {
this.fire('iron-resize');
}.bind(this), 500);
},
brightnessSliderChanged: function(ev, details, target) {
var bri = parseInt(target.value);
computeClassNames: function(stateObj) {
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
},
if(isNaN(bri)) return;
brightnessSliderChanged: function(ev) {
var bri = parseInt(ev.target.value);
if(bri === 0) {
serviceActions.callTurnOff(this.stateObj.entityId);
} else {
serviceActions.callService("light", "turn_on", {
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: function(ev) {
var color = ev.detail.rgb;
serviceActions.callService('light', 'turn_on', {
entity_id: this.stateObj.entityId,
brightness: bri
rgb_color: [color.r, color.g, color.b]
});
}
},
colorPicked: function(ev) {
var color = ev.detail.rgb;
serviceActions.callService("light", "turn_on", {
entity_id: this.stateObj.entityId,
rgb_color: [color.r, color.g, color.b]
});
}
});
});
})();
</script>
</polymer-element>

View File

@ -1,22 +1,26 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<polymer-element name="more-info-script" attributes="stateObj" noscript>
<template>
<core-style ref='ha-key-value-table'></core-style>
<style>
.data-entry .value {
max-width: 200px;
}
</style>
<div layout vertical>
<div layout justified horizontal class='data-entry'>
<div class='key'>Last Action</div>
<div class='value'>
{{stateObj.attributes.last_action}}
<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>
</div>
</template>
</polymer-element>
</template>
</dom-module>
<script>
(function() {
Polymer({
is: 'more-info-script',
properties: {
stateObj: {
type: Object,
},
},
});
})();
</script>

View File

@ -1,53 +1,71 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../components/relative-ha-datetime.html">
<polymer-element name="more-info-sun" attributes="stateObj">
<template>
<core-style ref='ha-key-value-table'></core-style>
<div layout vertical id='sunData'>
<div layout justified horizontal class='data-entry' id='rising'>
<dom-module id="more-info-sun">
<template>
<div class='data-entry layout justified horizontal' id='rising'>
<div class='key'>
Rising <relative-ha-datetime datetimeObj="{{rising}}"></relative-ha-datetime>
</div>
<div class='value'>
{{rising | formatTime}}
Rising <relative-ha-datetime datetime-obj="[[risingDate]]"></relative-ha-datetime>
</div>
<div class='value'>[[risingTime]]</div>
</div>
<div layout justified horizontal class='data-entry' id='setting'>
<div class='data-entry layout justified horizontal' id='setting'>
<div class='key'>
Setting <relative-ha-datetime datetimeObj="{{setting}}"></relative-ha-datetime>
</div>
<div class='value'>
{{setting | formatTime}}
Setting <relative-ha-datetime datetime-obj="[[settingDate]]"></relative-ha-datetime>
</div>
<div class='value'>[[settingTime]]</div>
</div>
</template>
</dom-module>
</div>
</template>
<script>
(function() {
var parseDateTime = window.hass.util.parseDateTime;
var parseDateTime = window.hass.util.parseDateTime;
var formatTime = window.hass.uiUtil.formatTime;
Polymer({
rising: null,
setting: null,
Polymer({
is: 'more-info-sun',
stateObjChanged: function() {
this.rising = parseDateTime(this.stateObj.attributes.next_rising);
this.setting = parseDateTime(this.stateObj.attributes.next_setting);
properties: {
stateObj: {
type: Object,
observer: 'stateObjChanged',
},
if(self.rising > self.setting) {
this.$.sunData.appendChild(this.$.rising);
} else {
this.$.sunData.appendChild(this.$.setting);
risingDate: {
type: Object,
},
settingDate: {
type: Object,
},
risingTime: {
type: String,
},
settingTime: {
type: String,
},
},
stateObjChanged: function() {
this.risingDate = parseDateTime(this.stateObj.attributes.next_rising);
this.risingTime = formatTime(this.risingDate);
this.settingDate = parseDateTime(this.stateObj.attributes.next_setting);
this.settingTime = formatTime(this.settingDate);
var root = Polymer.dom(this);
if(self.risingDate > self.settingDate) {
root.appendChild(this.$.rising);
} else {
root.appendChild(this.$.setting);
}
}
}
});
});
})();
</script>
</polymer-element>

View File

@ -1,114 +1,128 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-slider/paper-slider.html">
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/paper-slider/paper-slider.html'>
<link rel='import' href='../bower_components/paper-toggle-button/paper-toggle-button.html'>
<polymer-element name="more-info-thermostat" attributes="stateObj">
<template>
<dom-module id='more-info-thermostat'>
<style>
paper-slider {
width: 100%;
}
paper-slider::shadow #sliderKnobInner,
paper-slider::shadow #sliderBar::shadow #activeProgress {
background-color: #039be5;
}
.away-mode-toggle {
display: none;
margin-top: 16px;
}
:host-context(.has-away_mode) .away-mode-toggle {
.has-away_mode .away-mode-toggle {
display: block;
}
</style>
<template>
<div class$='[[computeClassNames(stateObj)]]'>
<div>
<div>Target Temperature</div>
<paper-slider
min='[[tempMin]]' max='[[tempMax]]'
value='[[targetTemperatureSliderValue]]' pin
on-change='targetTemperatureSliderChanged'>
</paper-slider>
</div>
<div>
<div>
<div>Target Temperature</div>
<paper-slider
min="{{tempMin}}" max="{{tempMax}}"
value='{{targetTemperatureSliderValue}}' pin
on-change="{{targetTemperatureSliderChanged}}">
</paper-slider>
</div>
<div class='away-mode-toggle'>
<div center horizontal layout>
<div flex>Away Mode</div>
<paper-toggle-button
checked="{{awayToggleChecked}}"
on-change="{{toggleChanged}}">
</paper-toggle-button>
<div class='away-mode-toggle'>
<div class='center horizontal layout'>
<div class='flex'>Away Mode</div>
<paper-toggle-button checked='[[awayToggleChecked]]' on-change='toggleChanged'>
</paper-toggle-button>
</div>
</div>
</div>
</div>
</template>
</template>
</dom-module>
<script>
var constants = window.hass.constants;
(function() {
var constants = window.hass.constants;
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['away_mode'];
Polymer({
tempMin: 10,
tempMax: 40,
targetTemperatureSliderValue: 0,
Polymer({
is: 'more-info-thermostat',
awayToggleChecked: false,
properties: {
stateObj: {
type: Object,
observer: 'stateObjChanged',
},
observe: {
'stateObj.attributes.away_mode': 'awayChanged'
},
tempMin: {
type: Number,
},
stateObjChanged: function(oldVal, newVal) {
this.targetTemperatureSliderValue = this.stateObj.state;
tempMax: {
type: Number,
},
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) {
this.tempMin = 45;
this.tempMax = 95;
} else {
this.tempMin = 7;
this.tempMax = 35;
}
},
targetTemperatureSliderValue: {
type: Number,
},
targetTemperatureSliderChanged: function(ev, details, target) {
var temp = parseInt(target.value);
awayToggleChecked: {
type: Boolean,
},
},
if(isNaN(temp)) return;
stateObjChanged: function(newVal, oldVal) {
this.targetTemperatureSliderValue = this.stateObj.state;
this.awayToggleChecked = this.stateObj.attributes.away_mode == 'on';
serviceActions.callService("thermostat", "set_temperature", {
entity_id: this.stateObj.entityId,
temperature: temp
});
},
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) {
this.tempMin = 45;
this.tempMax = 95;
} else {
this.tempMin = 7;
this.tempMax = 35;
}
},
toggleChanged: function(ev) {
var newVal = ev.target.checked;
computeClassNames: function(stateObj) {
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
},
if(newVal && this.stateObj.attributes.away_mode === 'off') {
this.service_set_away(true);
} else if(!newVal && this.stateObj.attributes.away_mode === 'on') {
this.service_set_away(false);
}
},
targetTemperatureSliderChanged: function(ev) {
var temp = parseInt(ev.target.value);
awayChanged: function(oldVal, newVal) {
this.awayToggleChecked = newVal == 'on';
},
if(isNaN(temp)) return;
service_set_away: function(away_mode) {
// We call stateChanged 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.callService(
'thermostat', 'set_away_mode',
{entity_id: this.stateObj.entityId, away_mode: away_mode})
serviceActions.callService('thermostat', 'set_temperature', {
entity_id: this.stateObj.entityId,
temperature: temp
});
},
.then(function() {
this.awayChanged(null, this.stateObj.attributes.away_mode);
}.bind(this));
},
});
toggleChanged: function(ev) {
var newVal = ev.target.checked;
if(newVal && this.stateObj.attributes.away_mode === 'off') {
this.service_set_away(true);
} else if(!newVal && this.stateObj.attributes.away_mode === 'on') {
this.service_set_away(false);
}
},
service_set_away: function(away_mode) {
// We call stateChanged 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.callService(
'thermostat', 'set_away_mode',
{entity_id: this.stateObj.entityId, away_mode: away_mode})
.then(function() {
this.stateObjChanged(this.stateObj);
}.bind(this));
},
});
})();
</script>
</polymer-element>

View File

@ -1,24 +1,22 @@
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/core-icons/social-icons.html">
<link rel="import" href="../bower_components/core-icons/image-icons.html">
<link rel="import" href="../bower_components/core-icons/hardware-icons.html">
<link rel="import" href="../bower_components/core-icons/av-icons.html">
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
<link rel="import" href="../bower_components/iron-icons/social-icons.html">
<link rel="import" href="../bower_components/iron-icons/image-icons.html">
<link rel="import" href="../bower_components/iron-icons/hardware-icons.html">
<link rel="import" href="../bower_components/iron-icons/av-icons.html">
<core-iconset-svg id="homeassistant-100" iconSize="100">
<iron-iconset-svg name="homeassistant-100" size="100">
<svg><defs>
<g id="thermostat">
<!--
Thermostat icon created by Scott Lewis from the Noun Project
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
-->
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
<g id="thermostat"><path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
</defs></svg>
</core-iconset-svg>
</iron-iconset-svg>
<core-iconset-svg id="homeassistant-24" iconSize="24">
<iron-iconset-svg name="homeassistant-24" size="24">
<svg><defs>
<!--
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
@ -29,9 +27,8 @@
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<g id="group"><path d="M9 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5-3c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-7c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
</defs></svg>
</core-iconset-svg>
</iron-iconset-svg>
<script>
window.hass.uiUtil.domainIcon = function(domain, state) {
@ -88,7 +85,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
return 'social:pages';
default:
return "bookmark-outline";
return "bookmark";
}
}
};
</script>

View File

@ -6,30 +6,25 @@
var DOMAINS_WITH_MORE_INFO = [
'light', 'group', 'sun', 'configurator', 'thermostat', 'script'
];
var DOMAINS_HIDE_MORE_INFO = [
'sensor',
];
// Add some frontend specific helpers to the models
Object.defineProperties(window.hass.stateModel.prototype, {
// how to render the card for this state
cardType: {
get: function() {
if(DOMAINS_WITH_CARD.indexOf(this.domain) !== -1) {
return this.domain;
} else if(this.canToggle) {
return "toggle";
} else {
return "display";
}
console.warn('Deprecated method. Please use hass.uiUtil.stateCardType');
return window.hass.uiUtil.stateCardType(this);
}
},
// how to render the more info of this state
moreInfoType: {
get: function() {
if(DOMAINS_WITH_MORE_INFO.indexOf(this.domain) !== -1) {
return this.domain;
} else {
return 'default';
}
console.warn('Deprecated method. Please use hass.uiUtil.stateMoreInfoType');
return window.hass.uiUtil.stateMoreInfoType(this);
}
},
});
@ -52,7 +47,7 @@
window.hass.uiActions = {
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
actionType: window.hass.uiConstants.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
@ -66,7 +61,34 @@
};
// UI specific util methods
window.hass.uiUtil = {};
window.hass.uiUtil = {
stateCardType: function(state) {
if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) {
return state.domain;
} else if(state.canToggle) {
return "toggle";
} else {
return "display";
}
},
stateMoreInfoType: function(state) {
if(DOMAINS_HIDE_MORE_INFO.indexOf(state.domain) !== -1) {
return false;
} else if(DOMAINS_WITH_MORE_INFO.indexOf(state.domain) !== -1) {
return state.domain;
} else {
return 'default';
}
},
attributeClassNames: function(stateObj, attributes) {
if (!stateObj) return '';
return attributes.map(function(attribute) {
return attribute in stateObj.attributes ? 'has-' + attribute : '';
}).join(' ');
},
};
})();
</script>

View File

@ -1,31 +1,23 @@
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/polymer/polymer.html">
<core-style id='ha-main'>
/* Palette generated by Material Palette - materialpalette.com/light-blue/orange */
<style is="custom-style">
:root {
--dark-primary-color: #0288D1;
--default-primary-color: #03A9F4;
--light-primary-color: #B3E5FC;
--text-primary-color: #ffffff;
--accent-color: #FF9800;
--primary-background-color: #ffffff;
--primary-text-color: #212121;
--secondary-text-color: #727272;
--disabled-text-color: #bdbdbd;
--divider-color: #B6B6B6;
.dark-primary-color { background: #0288D1; }
.default-primary-color { background: #03A9F4; }
.light-primary-color { background: #B3E5FC; }
.text-primary-color { color: #FFFFFF; }
.accent-color { background: #FF9800; }
.primary-text-color { color: #212121; }
.secondary-text-color { color: #727272; }
.divider-color { border-color: #B6B6B6; }
--paper-toggle-button-checked-ink-color: #039be5;
--paper-toggle-button-checked-button-color: #039be5;
--paper-toggle-button-checked-bar-color: #039be5;
}
/* extra */
.accent-text-color { color: #FF9800; }
body {
color: #212121;
}
a {
color: #FF9800;
text-decoration: none;
}
</core-style>
<core-style id='ha-animations'>
@-webkit-keyframes ha-spin {
0% {
-webkit-transform: rotate(0deg);
@ -47,106 +39,8 @@ a {
}
}
.ha-spin {
body /deep/ .ha-spin {
-webkit-animation: ha-spin 2s infinite linear;
animation: ha-spin 2s infinite linear;
}
</core-style>
<core-style id="ha-headers">
core-scroll-header-panel, core-header-panel {
background-color: #E5E5E5;
}
core-toolbar {
background: #03a9f4;
color: white;
font-weight: normal;
}
</core-style>
<core-style id="ha-dialog">
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
min-width: 350px;
max-width: 700px;
/* First two are from core-transition-bottom */
transition:
transform 0.2s ease-in-out,
opacity 0.2s ease-in,
top .3s,
left .3s !important;
}
:host .sidebar {
margin-left: 30px;
}
@media all and (max-width: 620px) {
:host.two-column {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
bottom: 0px;
left: 0px;
right: 0px;
}
:host .sidebar {
display: none;
}
}
@media all and (max-width: 464px) {
:host {
margin: 0;
width: 100%;
max-height: calc(100% - 64px);
bottom: 0px;
left: 0px;
right: 0px;
}
}
html /deep/ .ha-form paper-input {
display: block;
}
html /deep/ .ha-form paper-input:first-child {
padding-top: 0;
}
</core-style>
<core-style id='ha-key-value-table'>
.data-entry {
margin-bottom: 8px;
}
.data-entry:last-child {
margin-bottom: 0;
}
.data-entry .key {
margin-right: 8px;
}
.data-entry .value {
text-align: right;
word-break: break-all;
}
</core-style>
<core-style id='ha-paper-toggle'>
paper-toggle-button::shadow .toggle-ink {
color: #039be5;
}
paper-toggle-button::shadow [checked] .toggle-bar {
background-color: #039be5;
}
paper-toggle-button::shadow [checked] .toggle-button {
background-color: #039be5;
}
</core-style>
</style>

View File

@ -17,8 +17,4 @@ window.hass.uiUtil.formatDate = function(dateObj) {
return moment(dateObj).format('ll');
};
PolymerExpressions.prototype.formatTime = window.hass.uiUtil.formatTime;
PolymerExpressions.prototype.formatDateTime = window.hass.uiUtil.formatDateTime;
PolymerExpressions.prototype.formatDate = window.hass.uiUtil.formatDate;
</script>

View File

@ -0,0 +1,21 @@
<script>
(function() {
var StoreListenerMixIn = window.hass.storeListenerMixIn;
window.StoreListenerBehavior = {
attached: function() {
StoreListenerMixIn.listenToStores(true, this);
},
detached: function() {
StoreListenerMixIn.stopListeningToStores(this);
},
};
})();
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,23 +8,19 @@ fi
scripts/build_js $1
# To build the frontend, you need node, bower and vulcanize
# npm install -g bower vulcanize
# To build the frontend, you need node, bower, vulcanize and html-minifier
# npm install -g bower vulcanize html-minifier
# Install dependencies
cd homeassistant/components/frontend/www_static/polymer
bower install
cd ..
cp polymer/bower_components/webcomponentsjs/webcomponents.min.js .
cp polymer/bower_components/webcomponentsjs/webcomponents-lite.min.js .
# Let Polymer refer to the minified JS version before we compile
sed -i.bak 's/polymer\.js/polymer\.min\.js/' polymer/bower_components/polymer/polymer.html
vulcanize --inline-css --inline-scripts --strip-comments polymer/home-assistant.html > frontend.html
vulcanize -o frontend.html --inline --strip polymer/home-assistant.html
# Revert back the change to the Polymer component
rm polymer/bower_components/polymer/polymer.html
mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/polymer/polymer.html
# html-minifier crashes on frontend, minimize kills the CSS
# html-minifier --config-file polymer/html-minifier.conf -o frontend.html frontend.html
# Generate the MD5 hash of the new frontend
cd ..