Merge pull request #63 from home-assistant/inline-javascript

Clean up - step 2: Break up app in core + ui and inline some scripts
This commit is contained in:
Paulus Schoutsen 2016-05-28 16:15:07 -07:00
commit c1cbd33d41
59 changed files with 839 additions and 835 deletions

View File

@ -2,6 +2,15 @@
"extends": "airbnb-base", "extends": "airbnb-base",
"globals": { "globals": {
"__DEV__": false, "__DEV__": false,
"__DEMO__": false "Polymer": true
},
"rules": {
"new-cap": 0,
"prefer-template": 0,
"object-shorthand": 0,
"func-names": 0,
"prefer-arrow-callback": 0,
"no-underscore-dangle": 0,
"no-var": 0
} }
} }

23
src/app-core.js Normal file
View File

@ -0,0 +1,23 @@
import moment from 'moment';
import HomeAssistant from 'home-assistant-js';
window.moment = moment;
// While we figure out how ha-entity-marker can keep it's references
window.hass = new HomeAssistant();
window.validateAuth = function validateAuth(hass, authToken, rememberAuth) {
hass.authActions.validate(authToken, {
rememberAuth,
useStreaming: hass.localStoragePreferences.useStreaming,
});
};
window.removeInitMsg = function removeInitMessage() {
// remove the HTML init message
const initMsg = document.getElementById('ha-init-skeleton');
if (initMsg) {
initMsg.parentElement.removeChild(initMsg);
}
};

View File

@ -4,7 +4,6 @@ import dynamicContentUpdater from '../util/dynamic-content-updater';
require('./ha-camera-card'); require('./ha-camera-card');
require('./ha-entities-card'); require('./ha-entities-card');
require('./ha-introduction-card');
require('./ha-media_player-card'); require('./ha-media_player-card');
export default new Polymer({ export default new Polymer({

View File

@ -1,7 +1,6 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
import canToggle from '../util/can-toggle'; import canToggle from '../util/can-toggle';
require('../components/ha-card');
require('../components/entity/ha-entity-toggle'); require('../components/entity/ha-entity-toggle');
require('../state-summary/state-card-content'); require('../state-summary/state-card-content');

View File

@ -30,7 +30,7 @@
<template> <template>
<ha-card header="Welcome Home!"> <ha-card header="Welcome Home!">
<div class='content'> <div class='content'>
<template is='dom-if' if='[[showInstallInstruction]]'> <template is='dom-if' if='[[hass.demo]]'>
To install Home Assistant, run:<br /> To install Home Assistant, run:<br />
<code class='install'> <code class='install'>
pip3 install homeassistant<br /> pip3 install homeassistant<br />
@ -39,7 +39,7 @@
</template> </template>
Here are some resources to get started. Here are some resources to get started.
<ul> <ul>
<template is='dom-if' if='[[showInstallInstruction]]'> <template is='dom-if' if='[[hass.demo]]'>
<li><a href='https://home-assistant.io/getting-started/'>Home Assistant website</a></li> <li><a href='https://home-assistant.io/getting-started/'>Home Assistant website</a></li>
<li><a href='https://home-assistant.io/getting-started/'>Installation instructions</a></li> <li><a href='https://home-assistant.io/getting-started/'>Installation instructions</a></li>
<li><a href='https://home-assistant.io/getting-started/troubleshooting/'>Troubleshooting your installation</a></li> <li><a href='https://home-assistant.io/getting-started/troubleshooting/'>Troubleshooting your installation</a></li>
@ -64,3 +64,20 @@
</ha-card> </ha-card>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'ha-introduction-card',
properties: {
hass: {
type: Object,
},
showHideInstruction: {
type: Boolean,
value: true,
},
},
});
</script>

View File

@ -1,18 +0,0 @@
import Polymer from '../polymer';
require('../components/ha-card');
export default new Polymer({
is: 'ha-introduction-card',
properties: {
showInstallInstruction: {
type: Boolean,
value: __DEMO__,
},
showHideInstruction: {
type: Boolean,
value: true,
},
},
});

View File

@ -25,3 +25,34 @@
</ul> </ul>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'entity-list',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
entities: {
type: Array,
bindNuclear: function (hass) {
return [
hass.entityGetters.entityMap,
function (map) {
return map.valueSeq().sortBy(function (entity) { return entity.entityId; }).toArray();
},
];
},
},
},
entitySelected: function (ev) {
ev.preventDefault();
this.fire('entity-selected', { entityId: ev.model.entity.entityId });
},
});
</script>

View File

@ -1,26 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'entity-list',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
entities: {
type: Array,
bindNuclear: hass => [
hass.entityGetters.entityMap,
(map) => map.valueSeq().sortBy((entity) => entity.entityId).toArray(),
],
},
},
entitySelected(ev) {
ev.preventDefault();
this.fire('entity-selected', { entityId: ev.model.entity.entityId });
},
});

View File

@ -1,7 +1,5 @@
import Polymer from '../../polymer'; import Polymer from '../../polymer';
require('../../components/ha-label-badge');
/* /*
Leaflet clones this element before adding it to the map. This messes up Leaflet clones this element before adding it to the map. This messes up
our Poylmer object and we lose the reference to the `hass` object. our Poylmer object and we lose the reference to the `hass` object.

View File

@ -2,8 +2,6 @@ import Polymer from '../../polymer';
import domainIcon from '../../util/domain-icon'; import domainIcon from '../../util/domain-icon';
import stateIcon from '../../util/state-icon'; import stateIcon from '../../util/state-icon';
require('../../components/ha-label-badge');
export default new Polymer({ export default new Polymer({
is: 'ha-state-label-badge', is: 'ha-state-label-badge',

View File

@ -28,3 +28,34 @@
</ul> </ul>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'events-list',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
events: {
type: Array,
bindNuclear: function (hass) {
return [
hass.eventGetters.entityMap,
function (map) {
return map.valueSeq().sortBy(function (event) { return event.event; }).toArray();
},
];
},
},
},
eventSelected: function (ev) {
ev.preventDefault();
this.fire('event-selected', { eventType: ev.model.event.event });
},
});
</script>

View File

@ -1,26 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'events-list',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
events: {
type: Array,
bindNuclear: hass => [
hass.eventGetters.entityMap,
(map) => map.valueSeq().sortBy((event) => event.event).toArray(),
],
},
},
eventSelected(ev) {
ev.preventDefault();
this.fire('event-selected', { eventType: ev.model.event.event });
},
});

View File

@ -25,3 +25,23 @@
<content></content> <content></content>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'ha-card',
properties: {
header: {
type: String,
},
/**
* The z-depth of the card, from 0-5.
*/
elevation: {
type: Number,
value: 1,
reflectToAttribute: true,
},
},
});
</script>

View File

@ -1,19 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'ha-card',
properties: {
header: {
type: String,
},
/**
* The z-depth of the card, from 0-5.
*/
elevation: {
type: Number,
value: 1,
reflectToAttribute: true,
},
},
});

View File

@ -1,6 +1,5 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('.//ha-demo-badge');
require('../cards/ha-badges-card'); require('../cards/ha-badges-card');
require('../cards/ha-card-chooser'); require('../cards/ha-card-chooser');
@ -109,8 +108,9 @@ export default new Polymer({
} }
if (showIntroduction) { if (showIntroduction) {
cards.columns[getIndex(5)].push({ cards.columns[getIndex(5)].push({
hass,
cardType: 'introduction', cardType: 'introduction',
showHideInstruction: states.size > 0 && !__DEMO__, showHideInstruction: states.size > 0 && !hass.demo,
}); });
} }

View File

@ -10,3 +10,134 @@
<canvas width='[[width]]' height='[[height]]'></canvas> <canvas width='[[width]]' height='[[height]]'></canvas>
</template> </template>
</dom-module> </dom-module>
<script>
/**
* Color-picker custom element
* Originally created by bbrewer97202 (Ben Brewer). MIT Licensed.
* https://github.com/bbrewer97202/color-picker-element
*
* Adapted to work with Polymer.
*/
Polymer({
is: 'ha-color-picker',
properties: {
color: {
type: Object,
},
width: {
type: Number,
},
height: {
type: Number,
},
},
listeners: {
mousedown: 'onMouseDown',
mouseup: 'onMouseUp',
touchstart: 'onTouchStart',
touchend: 'onTouchEnd',
},
onMouseDown: function (ev) {
this.onMouseMove(ev);
this.addEventListener('mousemove', this.onMouseMove);
},
onMouseUp: function () {
this.removeEventListener('mousemove', this.onMouseMove);
},
onTouchStart: function (ev) {
this.onTouchMove(ev);
this.addEventListener('touchmove', this.onTouchMove);
},
onTouchEnd: function () {
this.removeEventListener('touchmove', this.onTouchMove);
},
onTouchMove: function (ev) {
if (!this.mouseMoveIsThrottled) {
return;
}
this.mouseMoveIsThrottled = false;
this.processColorSelect(ev.touches[0]);
this.async(function () { this.mouseMoveIsThrottled = true; }.bind(this), 100);
},
onMouseMove: function (ev) {
if (!this.mouseMoveIsThrottled) {
return;
}
this.mouseMoveIsThrottled = false;
this.processColorSelect(ev);
this.async(function () { this.mouseMoveIsThrottled = true; }.bind(this), 100);
},
processColorSelect: function (ev) {
var rect = this.canvas.getBoundingClientRect();
// boundary check because people can move off-canvas.
if (ev.clientX < rect.left || ev.clientX >= rect.left + rect.width ||
ev.clientY < rect.top || ev.clientY >= rect.top + rect.height) {
return;
}
this.onColorSelect(ev.clientX - rect.left, ev.clientY - rect.top);
},
onColorSelect: function (x, y) {
var data = this.context.getImageData(x, y, 1, 1).data;
this.setColor({ r: data[0], g: data[1], b: data[2] });
},
setColor: function (rgb) {
this.color = rgb;
this.fire('colorselected', { rgb: this.color });
},
ready: function () {
this.setColor = this.setColor.bind(this);
this.mouseMoveIsThrottled = true;
this.canvas = this.children[0];
this.context = this.canvas.getContext('2d');
this.drawGradient();
},
drawGradient: function () {
var style;
if (!this.width || !this.height) {
style = getComputedStyle(this);
}
var width = this.width || parseInt(style.width, 10);
var height = this.height || parseInt(style.height, 10);
var colorGradient = this.context.createLinearGradient(0, 0, width, 0);
colorGradient.addColorStop(0, 'rgb(255,0,0)');
colorGradient.addColorStop(0.16, 'rgb(255,0,255)');
colorGradient.addColorStop(0.32, 'rgb(0,0,255)');
colorGradient.addColorStop(0.48, 'rgb(0,255,255)');
colorGradient.addColorStop(0.64, 'rgb(0,255,0)');
colorGradient.addColorStop(0.80, 'rgb(255,255,0)');
colorGradient.addColorStop(1, 'rgb(255,0,0)');
this.context.fillStyle = colorGradient;
this.context.fillRect(0, 0, width, height);
var bwGradient = this.context.createLinearGradient(0, 0, 0, height);
bwGradient.addColorStop(0, 'rgba(255,255,255,1)');
bwGradient.addColorStop(0.5, 'rgba(255,255,255,0)');
bwGradient.addColorStop(0.5, 'rgba(0,0,0,0)');
bwGradient.addColorStop(1, 'rgba(0,0,0,1)');
this.context.fillStyle = bwGradient;
this.context.fillRect(0, 0, width, height);
},
});
</script>

View File

@ -1,130 +0,0 @@
/**
* Color-picker custom element
* Originally created by bbrewer97202 (Ben Brewer). MIT Licensed.
* https://github.com/bbrewer97202/color-picker-element
*
* Adapted to work with Polymer.
*/
import Polymer from '../polymer';
export default new Polymer({
is: 'ha-color-picker',
properties: {
color: {
type: Object,
},
width: {
type: Number,
},
height: {
type: Number,
},
},
listeners: {
mousedown: 'onMouseDown',
mouseup: 'onMouseUp',
touchstart: 'onTouchStart',
touchend: 'onTouchEnd',
},
onMouseDown(ev) {
this.onMouseMove(ev);
this.addEventListener('mousemove', this.onMouseMove);
},
onMouseUp() {
this.removeEventListener('mousemove', this.onMouseMove);
},
onTouchStart(ev) {
this.onTouchMove(ev);
this.addEventListener('touchmove', this.onTouchMove);
},
onTouchEnd() {
this.removeEventListener('touchmove', this.onTouchMove);
},
onTouchMove(ev) {
if (!this.mouseMoveIsThrottled) {
return;
}
this.mouseMoveIsThrottled = false;
this.processColorSelect(ev.touches[0]);
this.async(() => { this.mouseMoveIsThrottled = true; }, 100);
},
onMouseMove(ev) {
if (!this.mouseMoveIsThrottled) {
return;
}
this.mouseMoveIsThrottled = false;
this.processColorSelect(ev);
this.async(() => { this.mouseMoveIsThrottled = true; }, 100);
},
processColorSelect(ev) {
const rect = this.canvas.getBoundingClientRect();
// boundary check because people can move off-canvas.
if (ev.clientX < rect.left || ev.clientX >= rect.left + rect.width ||
ev.clientY < rect.top || ev.clientY >= rect.top + rect.height) {
return;
}
this.onColorSelect(ev.clientX - rect.left, ev.clientY - rect.top);
},
onColorSelect(x, y) {
const data = this.context.getImageData(x, y, 1, 1).data;
this.setColor({ r: data[0], g: data[1], b: data[2] });
},
setColor(rgb) {
this.color = rgb;
this.fire('colorselected', { rgb: this.color });
},
ready() {
this.setColor = this.setColor.bind(this);
this.mouseMoveIsThrottled = true;
this.canvas = this.children[0];
this.context = this.canvas.getContext('2d');
this.drawGradient();
},
drawGradient() {
let style;
if (!this.width || !this.height) {
style = getComputedStyle(this);
}
const width = this.width || parseInt(style.width, 10);
const height = this.height || parseInt(style.height, 10);
const colorGradient = this.context.createLinearGradient(0, 0, width, 0);
colorGradient.addColorStop(0, 'rgb(255,0,0)');
colorGradient.addColorStop(0.16, 'rgb(255,0,255)');
colorGradient.addColorStop(0.32, 'rgb(0,0,255)');
colorGradient.addColorStop(0.48, 'rgb(0,255,255)');
colorGradient.addColorStop(0.64, 'rgb(0,255,0)');
colorGradient.addColorStop(0.80, 'rgb(255,255,0)');
colorGradient.addColorStop(1, 'rgb(255,0,0)');
this.context.fillStyle = colorGradient;
this.context.fillRect(0, 0, width, height);
const bwGradient = this.context.createLinearGradient(0, 0, 0, height);
bwGradient.addColorStop(0, 'rgba(255,255,255,1)');
bwGradient.addColorStop(0.5, 'rgba(255,255,255,0)');
bwGradient.addColorStop(0.5, 'rgba(0,0,0,0)');
bwGradient.addColorStop(1, 'rgba(0,0,0,1)');
this.context.fillStyle = bwGradient;
this.context.fillRect(0, 0, width, height);
},
});

View File

@ -16,3 +16,9 @@
></ha-label-badge> ></ha-label-badge>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'ha-demo-badge',
});
</script>

View File

@ -1,7 +0,0 @@
import Polymer from '../polymer';
require('./ha-label-badge');
export default new Polymer({
is: 'ha-demo-badge',
});

View File

@ -83,3 +83,54 @@
</div> </div>
</template> </template>
</dom-module> </dom-module>
<script>
// Beware: Polymer will not call computeHideIcon and computeHideValue if any of
// the parameters are undefined. Set to null if not using.
Polymer({
is: 'ha-label-badge',
properties: {
value: {
type: String,
value: null,
},
icon: {
type: String,
value: null,
},
label: {
type: String,
value: null,
},
description: {
type: String,
},
image: {
type: String,
value: null,
observer: 'imageChanged',
},
},
computeClasses: function (value) {
return value && value.length > 4 ? 'value big' : 'value';
},
computeHideIcon: function (icon, value, image) {
return !icon || value || image;
},
computeHideValue: function (value, image) {
return !value || image;
},
imageChanged: function (newVal) {
this.$.badge.style.backgroundImage = newVal ? `url(${newVal})` : '';
},
});
</script>

View File

@ -1,50 +0,0 @@
import Polymer from '../polymer';
// Beware: Polymer will not call computeHideIcon and computeHideValue if any of
// the parameters are undefined. Set to null if not using.
export default new Polymer({
is: 'ha-label-badge',
properties: {
value: {
type: String,
value: null,
},
icon: {
type: String,
value: null,
},
label: {
type: String,
value: null,
},
description: {
type: String,
},
image: {
type: String,
value: null,
observer: 'imageChanged',
},
},
computeClasses(value) {
return value && value.length > 4 ? 'value big' : 'value';
},
computeHideIcon(icon, value, image) {
return !icon || value || image;
},
computeHideValue(value, image) {
return !value || image;
},
imageChanged(newVal) {
this.$.badge.style.backgroundImage = newVal ? `url(${newVal})` : '';
},
});

View File

@ -14,7 +14,28 @@
No logbook entries found. No logbook entries found.
</template> </template>
<template is='dom-repeat' items="[[entries]]"> <template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry> <logbook-entry entry-obj="[[item]]" hass='[[hass]]'></logbook-entry>
</template> </template>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'ha-logbook',
properties: {
hass: {
type: Object,
},
entries: {
type: Array,
value: [],
},
},
noEntries: function (entries) {
return !entries.length;
},
});
</script>

View File

@ -1,18 +0,0 @@
import Polymer from '../polymer';
require('./logbook-entry');
export default new Polymer({
is: 'ha-logbook',
properties: {
entries: {
type: Object,
value: [],
},
},
noEntries(entries) {
return !entries.length;
},
});

View File

@ -169,3 +169,86 @@
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'ha-sidebar',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
menuShown: {
type: Boolean,
},
menuSelected: {
type: String,
},
narrow: {
type: Boolean,
},
selected: {
type: String,
bindNuclear: function (hass) { return hass.navigationGetters.activePane; },
},
hasHistoryComponent: {
type: Boolean,
bindNuclear: function (hass) { return hass.configGetters.isComponentLoaded('history'); },
},
hasLogbookComponent: {
type: Boolean,
bindNuclear: function (hass) { return hass.configGetters.isComponentLoaded('logbook'); },
},
},
created: function () {
this._boundUpdateStyles = this.updateStyles.bind(this);
},
menuSelect: function () {
this.debounce('updateStyles', this._boundUpdateStyles, 1);
},
menuClicked: function (ev) {
var target = ev.target;
var checks = 5;
// find panel to select
while (checks && !target.getAttribute('data-panel')) {
target = target.parentElement;
checks--;
}
if (checks) {
this.selectPanel(target.getAttribute('data-panel'));
}
},
toggleMenu: function () {
this.fire('close-menu');
},
selectPanel: function (newChoice) {
if (newChoice === this.selected) {
return;
} else if (newChoice === 'logout') {
this.handleLogOut();
return;
}
this.hass.navigationActions.navigate.apply(null, newChoice.split('/'));
this.debounce('updateStyles', this._boundUpdateStyles, 1);
},
handleLogOut: function () {
this.hass.authActions.logOut();
},
});
</script>

View File

@ -1,80 +0,0 @@
import Polymer from '../polymer';
require('./stream-status');
export default new Polymer({
is: 'ha-sidebar',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
menuShown: {
type: Boolean,
},
menuSelected: {
type: String,
},
narrow: {
type: Boolean,
},
selected: {
type: String,
bindNuclear: hass => hass.navigationGetters.activePane,
},
hasHistoryComponent: {
type: Boolean,
bindNuclear: hass => hass.configGetters.isComponentLoaded('history'),
},
hasLogbookComponent: {
type: Boolean,
bindNuclear: hass => hass.configGetters.isComponentLoaded('logbook'),
},
},
menuSelect() {
this.debounce('updateStyles', () => this.updateStyles(), 1);
},
menuClicked(ev) {
let target = ev.target;
let checks = 5;
// find panel to select
while (checks && !target.getAttribute('data-panel')) {
target = target.parentElement;
checks--;
}
if (checks) {
this.selectPanel(target.getAttribute('data-panel'));
}
},
toggleMenu() {
this.fire('close-menu');
},
selectPanel(newChoice) {
if (newChoice === this.selected) {
return;
} else if (newChoice === 'logout') {
this.handleLogOut();
return;
}
this.hass.navigationActions.navigate.apply(null, newChoice.split('/'));
this.debounce('updateStyles', () => this.updateStyles(), 1);
},
handleLogOut() {
this.hass.authActions.logOut();
},
});

View File

@ -23,3 +23,58 @@
</paper-tabs> </paper-tabs>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'ha-view-tabs',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
locationName: {
type: String,
bindNuclear: function (hass) { return hass.configGetters.locationName; },
},
currentView: {
type: String,
bindNuclear: function (hass) {
return [
hass.viewGetters.currentView,
function (view) { return view || ''; },
];
},
},
views: {
type: Array,
bindNuclear: function (hass) {
return [
hass.viewGetters.views,
function (views) {
return views.valueSeq()
.sortBy(function (view) { return view.attributes.order; })
.toArray();
},
];
},
},
},
viewTapped() {
this.fire('view-tapped');
},
viewSelected(ev) {
var view = ev.detail.item.getAttribute('data-entity') || null;
var current = this.currentView || null;
if (view !== current) {
this.async(function () { this.hass.viewActions.selectView(view); }.bind(this), 0);
}
},
});
</script>

View File

@ -1,49 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'ha-view-tabs',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
locationName: {
type: String,
bindNuclear: hass => hass.configGetters.locationName,
},
currentView: {
type: String,
bindNuclear: hass => [
hass.viewGetters.currentView,
view => view || '',
],
},
views: {
type: Array,
bindNuclear: hass => [
hass.viewGetters.views,
views => views.valueSeq()
.sortBy(view => view.attributes.order)
.toArray(),
],
},
},
viewTapped() {
this.fire('view-tapped');
},
viewSelected(ev) {
const view = ev.detail.item.getAttribute('data-entity') || null;
const current = this.currentView || null;
this.expectChange = true;
if (view !== current) {
this.async(() => this.hass.viewActions.selectView(view), 0);
}
},
});

View File

@ -18,3 +18,9 @@
</div> </div>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'loading-box',
});
</script>

View File

@ -1,5 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'loading-box',
});

View File

@ -2,7 +2,6 @@ import Polymer from '../polymer';
require('./domain-icon'); require('./domain-icon');
require('./display-time'); require('./display-time');
require('./relative-ha-datetime');
export default new Polymer({ export default new Polymer({
is: 'logbook-entry', is: 'logbook-entry',

View File

@ -5,3 +5,60 @@
<span>[[relativeTime]]</span> <span>[[relativeTime]]</span>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'relative-ha-datetime',
properties: {
datetime: {
type: String,
observer: 'datetimeChanged',
},
datetimeObj: {
type: Object,
observer: 'datetimeObjChanged',
},
parsedDateTime: {
type: Object,
},
relativeTime: {
type: String,
value: 'not set',
},
},
created: function () {
this.updateRelative = this.updateRelative.bind(this);
},
attached: function () {
// update every 60 seconds
this.updateInterval = setInterval(this.updateRelative, 60000);
},
detached: function () {
clearInterval(this.updateInterval);
},
datetimeChanged: function (newVal) {
this.parsedDateTime = newVal ? new Date(newVal) : null;
this.updateRelative();
},
datetimeObjChanged: function (newVal) {
this.parsedDateTime = newVal;
this.updateRelative();
},
updateRelative: function () {
this.relativeTime = this.parsedDateTime ?
window.moment(this.parsedDateTime).fromNow() : '';
},
});
</script>

View File

@ -1,57 +0,0 @@
import Polymer from '../polymer';
const UPDATE_INTERVAL = 60000; // 60 seconds
export default new Polymer({
is: 'relative-ha-datetime',
properties: {
datetime: {
type: String,
observer: 'datetimeChanged',
},
datetimeObj: {
type: Object,
observer: 'datetimeObjChanged',
},
parsedDateTime: {
type: Object,
},
relativeTime: {
type: String,
value: 'not set',
},
},
created() {
this.updateRelative = this.updateRelative.bind(this);
},
attached() {
this.updateInterval = setInterval(this.updateRelative, UPDATE_INTERVAL);
},
detached() {
clearInterval(this.updateInterval);
},
datetimeChanged(newVal) {
this.parsedDateTime = newVal ? new Date(newVal) : null;
this.updateRelative();
},
datetimeObjChanged(newVal) {
this.parsedDateTime = newVal;
this.updateRelative();
},
updateRelative() {
this.relativeTime = this.parsedDateTime ?
window.moment(this.parsedDateTime).fromNow() : '';
},
});

View File

@ -1,6 +1,5 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('./loading-box');
require('./state-history-chart-timeline'); require('./state-history-chart-timeline');
require('./state-history-chart-line'); require('./state-history-chart-line');

View File

@ -1,7 +1,6 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('./entity/state-badge'); require('./entity/state-badge');
require('./relative-ha-datetime');
export default new Polymer({ export default new Polymer({
is: 'state-info', is: 'state-info',

View File

@ -25,3 +25,35 @@
<paper-toggle-button id="toggle" on-change='toggleChanged' checked$='[[isStreaming]]' hidden$="[[hasError]]"></paper-toggle-button> <paper-toggle-button id="toggle" on-change='toggleChanged' checked$='[[isStreaming]]' hidden$="[[hasError]]"></paper-toggle-button>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'stream-status',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
isStreaming: {
type: Boolean,
bindNuclear: function (hass) { return hass.streamGetters.isStreamingEvents; },
},
hasError: {
type: Boolean,
bindNuclear: function (hass) { return hass.streamGetters.hasStreamingEventsError; },
},
},
toggleChanged: function () {
if (this.isStreaming) {
this.hass.streamActions.stop();
} else {
this.hass.streamActions.start();
}
},
});
</script>

View File

@ -1,31 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'stream-status',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
isStreaming: {
type: Boolean,
bindNuclear: hass => hass.streamGetters.isStreamingEvents,
},
hasError: {
type: Boolean,
bindNuclear: hass => hass.streamGetters.hasStreamingEventsError,
},
},
toggleChanged() {
if (this.isStreaming) {
this.hass.streamActions.stop();
} else {
this.hass.streamActions.start();
}
},
});

View File

@ -1,14 +1,17 @@
<link rel='import' href='../bower_components/polymer/polymer.html'> <link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/paper-spinner/paper-spinner.html'> <link rel='import' href='../bower_components/paper-spinner/paper-spinner.html'>
<script src='../build/_app_core_compiled.js'></script>
<link rel='import' href='../bower_components/iron-flex-layout/iron-flex-layout-classes.html'> <link rel='import' href='../bower_components/iron-flex-layout/iron-flex-layout-classes.html'>
<link rel='import' href='./util/roboto.html'> <link rel='import' href='./util/roboto.html'>
<link rel='import' href='../bower_components/paper-styles/typography.html'> <link rel='import' href='../bower_components/paper-styles/typography.html'>
<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html"> <link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html">
<link rel='import' href='./util/hass-behavior.html'>
<link rel='import' href='./layouts/login-form.html'> <link rel='import' href='./layouts/login-form.html'>
<link rel='import' href='./layouts/home-assistant-main.html'> <link rel='import' href='./layouts/home-assistant-main.html'>
<link rel='import' href='./resources/home-assistant-style.html'>
<link rel='import' href='resources/home-assistant-style.html'>
<dom-module id='home-assistant'> <dom-module id='home-assistant'>
<style> <style>
@ -29,9 +32,94 @@
</template> </template>
<template is='dom-if' if='[[loaded]]'> <template is='dom-if' if='[[loaded]]'>
<home-assistant-main hass='[[hass]]'></home-assistant-main> <home-assistant-main hass='[[hass]]' hidden$='[[!loaded]]'></home-assistant-main>
</template> </template>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'home-assistant',
hostAttributes: {
auth: null,
icons: null,
},
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
value: window.hass,
},
auth: {
type: String,
},
icons: {
type: String,
},
dataLoaded: {
type: Boolean,
bindNuclear: function (hass) { return hass.syncGetters.isDataLoaded; },
},
iconsLoaded: {
type: Boolean,
value: false,
},
loaded: {
type: Boolean,
computed: 'computeLoaded(dataLoaded, iconsLoaded)',
},
},
computeLoaded: function (dataLoaded, iconsLoaded) {
return dataLoaded && iconsLoaded;
},
computeForceShowLoading: function (dataLoaded, iconsLoaded) {
return dataLoaded && !iconsLoaded;
},
loadIcons: function () {
// If the import fails, we'll try to import again, must be a server glitch
// Since HTML imports only resolve once, we import another url.
var success = function () {
this.iconsLoaded = true;
}.bind(this);
this.importHref(`/static/mdi-${this.icons}.html`,
success,
function () {
this.importHref('/static/mdi.html', success, success);
});
},
created: function () {
if (!('serviceWorker' in navigator)) {
return;
}
navigator.serviceWorker.register('/service_worker.js');
},
ready: function () {
var hass = this.hass;
hass.reactor.batch(function () {
// if auth was given, tell the backend
if (this.auth) {
window.validateAuth(this.hass, this.auth, false);
} else if (hass.localStoragePreferences.authToken) {
window.validateAuth(this.hass, hass.localStoragePreferences.authToken, true);
}
hass.navigationActions.showSidebar(hass.localStoragePreferences.showSidebar);
});
hass.startLocalStoragePreferencesSync();
this.loadIcons();
},
});
</script>
<script src='../build/_app_compiled.js'></script> <script src='../build/_app_compiled.js'></script>

View File

@ -1,99 +1 @@
import moment from 'moment';
import Polymer from './polymer';
import HomeAssistant from 'home-assistant-js';
import validateAuth from './util/validate-auth';
import hassBehavior from './util/hass-behavior';
window.hassBehavior = hassBehavior;
window.moment = moment;
require('./layouts/login-form');
require('./layouts/home-assistant-main'); require('./layouts/home-assistant-main');
// While we figure out how ha-entity-marker can keep it's references
window.hass = new HomeAssistant();
export default new Polymer({
is: 'home-assistant',
hostAttributes: {
auth: null,
icons: null,
},
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
value: window.hass,
},
auth: {
type: String,
},
icons: {
type: String,
},
dataLoaded: {
type: Boolean,
bindNuclear: hass => hass.syncGetters.isDataLoaded,
},
iconsLoaded: {
type: Boolean,
value: false,
},
loaded: {
type: Boolean,
computed: 'computeLoaded(dataLoaded, iconsLoaded)',
},
},
computeLoaded(dataLoaded, iconsLoaded) {
return dataLoaded && iconsLoaded;
},
computeForceShowLoading(dataLoaded, iconsLoaded) {
return dataLoaded && !iconsLoaded;
},
loadIcons() {
// If the import fails, we'll try to import again, must be a server glitch
// Since HTML imports only resolve once, we import another url.
const success = () => { this.iconsLoaded = true; };
this.importHref(`/static/mdi-${this.icons}.html`,
success,
() => this.importHref('/static/mdi.html', success, success));
},
created() {
if (!('serviceWorker' in navigator)) {
return;
}
navigator.serviceWorker.register('/service_worker.js').catch(err => {
if (__DEV__) {
/* eslint-disable no-console */
console.warn('Unable to register service worker', err);
/* eslint-enable no-console */
}
});
},
ready() {
const hass = this.hass;
hass.reactor.batch(() => {
// if auth was given, tell the backend
if (this.auth) {
validateAuth(this.hass, this.auth, false);
} else if (hass.localStoragePreferences.authToken) {
validateAuth(this.hass, hass.localStoragePreferences.authToken, true);
}
hass.navigationActions.showSidebar(hass.localStoragePreferences.showSidebar);
});
hass.startLocalStoragePreferencesSync();
this.loadIcons();
},
});

View File

@ -1,8 +1,5 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
import removeInitMsg from '../util/remove-init-message';
require('../components/ha-sidebar');
require('../layouts/partial-cards'); require('../layouts/partial-cards');
require('../layouts/partial-logbook'); require('../layouts/partial-logbook');
require('../layouts/partial-history'); require('../layouts/partial-history');
@ -16,13 +13,6 @@ require('../managers/notification-manager');
require('../dialogs/more-info-dialog'); require('../dialogs/more-info-dialog');
require('../dialogs/ha-voice-command-dialog'); require('../dialogs/ha-voice-command-dialog');
// const {
// navigationActions,
// navigationGetters,
// startUrlSync,
// stopUrlSync,
// } = hass;
export default new Polymer({ export default new Polymer({
is: 'home-assistant-main', is: 'home-assistant-main',
@ -35,7 +25,7 @@ export default new Polymer({
narrow: { narrow: {
type: Boolean, type: Boolean,
value: false, value: true,
}, },
activePane: { activePane: {
@ -91,6 +81,7 @@ export default new Polymer({
showSidebar: { showSidebar: {
type: Boolean, type: Boolean,
value: false,
bindNuclear: hass => hass.navigationGetters.showSidebar, bindNuclear: hass => hass.navigationGetters.showSidebar,
}, },
}, },
@ -122,7 +113,7 @@ export default new Polymer({
}, },
attached() { attached() {
removeInitMsg(); window.removeInitMsg();
this.hass.startUrlSync(); this.hass.startUrlSync();
}, },

View File

@ -65,3 +65,95 @@
</div> </div>
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'login-form',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
errorMessage: {
type: String,
bindNuclear: function (hass) { return hass.authGetters.attemptErrorMessage; },
},
isInvalid: {
type: Boolean,
bindNuclear: function (hass) { return hass.authGetters.isInvalidAttempt; },
},
isValidating: {
type: Boolean,
observer: 'isValidatingChanged',
bindNuclear: function (hass) { return hass.authGetters.isValidating; },
},
loadingResources: {
type: Boolean,
value: false,
},
forceShowLoading: {
type: Boolean,
value: false,
},
showLoading: {
type: Boolean,
computed: 'computeShowSpinner(forceShowLoading, isValidating)',
},
},
listeners: {
keydown: 'passwordKeyDown',
'loginButton.tap': 'validatePassword',
},
observers: [
'validatingChanged(isValidating, isInvalid)',
],
attached: function () {
window.removeInitMsg();
},
computeShowSpinner: function (forceShowLoading, isValidating) {
return forceShowLoading || isValidating;
},
validatingChanged: function (isValidating, isInvalid) {
if (!isValidating && !isInvalid) {
this.$.passwordInput.value = '';
}
},
isValidatingChanged: function (newVal) {
if (!newVal) {
this.async(function () { this.$.passwordInput.focus(); }.bind(this), 10);
}
},
passwordKeyDown: function (ev) {
// validate on enter
if (ev.keyCode === 13) {
this.validatePassword();
ev.preventDefault();
// clear error after we start typing again
} else if (this.isInvalid) {
this.isInvalid = false;
}
},
validatePassword: function () {
this.$.hideKeyboardOnFocus.focus();
window.validateAuth(this.hass, this.$.passwordInput.value,
this.$.rememberLogin.checked);
},
});
</script>

View File

@ -1,93 +0,0 @@
import Polymer from '../polymer';
import validateAuth from '../util/validate-auth';
import removeInitMsg from '../util/remove-init-message';
export default new Polymer({
is: 'login-form',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
errorMessage: {
type: String,
bindNuclear: hass => hass.authGetters.attemptErrorMessage,
},
isInvalid: {
type: Boolean,
bindNuclear: hass => hass.authGetters.isInvalidAttempt,
},
isValidating: {
type: Boolean,
observer: 'isValidatingChanged',
bindNuclear: hass => hass.authGetters.isValidating,
},
loadingResources: {
type: Boolean,
value: false,
},
forceShowLoading: {
type: Boolean,
value: false,
},
showLoading: {
type: Boolean,
computed: 'computeShowSpinner(forceShowLoading, isValidating)',
},
},
listeners: {
keydown: 'passwordKeyDown',
'loginButton.tap': 'validatePassword',
},
observers: [
'validatingChanged(isValidating, isInvalid)',
],
attached() {
removeInitMsg();
},
computeShowSpinner(forceShowLoading, isValidating) {
return forceShowLoading || isValidating;
},
validatingChanged(isValidating, isInvalid) {
if (!isValidating && !isInvalid) {
this.$.passwordInput.value = '';
}
},
isValidatingChanged(newVal) {
if (!newVal) {
this.async(() => this.$.passwordInput.focus(), 10);
}
},
passwordKeyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
this.validatePassword();
ev.preventDefault();
// clear error after we start typing again
} else if (this.isInvalid) {
this.isInvalid = false;
}
},
validatePassword() {
this.$.hideKeyboardOnFocus.focus();
validateAuth(this.hass, this.$.passwordInput.value, this.$.rememberLogin.checked);
},
});

View File

@ -2,7 +2,6 @@ import Polymer from '../polymer';
require('./partial-base'); require('./partial-base');
require('../components/ha-cards'); require('../components/ha-cards');
require('../components/ha-view-tabs');
export default new Polymer({ export default new Polymer({
is: 'partial-cards', is: 'partial-cards',

View File

@ -55,7 +55,7 @@
<div> <div>
<div class='header'>Available services</div> <div class='header'>Available services</div>
<services-list on-service-selected='serviceSelected'></services-list> <services-list on-service-selected='serviceSelected' hass='[[hass]]'></services-list>
</div> </div>
</div> </div>
</partial-base> </partial-base>

View File

@ -49,7 +49,7 @@
<div> <div>
<div class='header'>Available Events</div> <div class='header'>Available Events</div>
<events-list on-event-selected='eventSelected'></event-list> <events-list on-event-selected='eventSelected' hass='[[hass]]'></event-list>
</div> </div>
</div> </div>
</partial-base> </partial-base>

View File

@ -1,7 +1,6 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('./partial-base'); require('./partial-base');
require('../components/events-list');
export default new Polymer({ export default new Polymer({
is: 'partial-dev-fire-event', is: 'partial-dev-fire-event',

View File

@ -51,7 +51,7 @@
<div> <div>
<div class='header'>Current entities</div> <div class='header'>Current entities</div>
<entity-list on-entity-selected='entitySelected'></entity-list> <entity-list on-entity-selected='entitySelected' hass='[[hass]]'></entity-list>
</div> </div>
</div> </div>
</partial-base> </partial-base>

View File

@ -1,7 +1,6 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('./partial-base'); require('./partial-base');
require('../components/entity-list');
export default new Polymer({ export default new Polymer({
is: 'partial-dev-set-state', is: 'partial-dev-set-state',

View File

@ -34,7 +34,7 @@
<loading-box hidden$='[[!isLoading]]'>Loading logbook entries</loading-box> <loading-box hidden$='[[!isLoading]]'>Loading logbook entries</loading-box>
</div> </div>
<ha-logbook entries="[[entries]]" hidden$='[[isLoading]]'></ha-logbook> <ha-logbook hass='[[hass]]' entries="[[entries]]" hidden$='[[isLoading]]'></ha-logbook>
</div> </div>
</partial-base> </partial-base>
</template> </template>

View File

@ -1,8 +1,7 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('./partial-base'); require('./partial-base');
require('../components/ha-logbook'); require('../components/logbook-entry');
require('../components/loading-box');
export default new Polymer({ export default new Polymer({
is: 'partial-logbook', is: 'partial-logbook',

View File

@ -12,7 +12,38 @@
</style> </style>
<template> <template>
<img class='camera-image' src="[[computeCameraImageUrl(stateObj)]]" <img class='camera-image' src="[[computeCameraImageUrl(hass, stateObj)]]"
on-load='imageLoaded' /> on-load='imageLoaded' />
</template> </template>
</dom-module> </dom-module>
<script>
Polymer({
is: 'more-info-camera',
properties: {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
},
imageLoaded: function () {
this.fire('iron-resize');
},
computeCameraImageUrl: function (hass, stateObj) {
if (hass.demo) {
return '/demo/webcam.jpg';
} else if (stateObj) {
return `/api/camera_proxy_stream/${stateObj.entityId}` +
`?token=${stateObj.attributes.access_token}`;
}
// Return an empty image if no stateObj (= dialog not open)
return 'data:image/gif;base64,R0lGODlhAQABAAAAACw=';
},
});
</script>

View File

@ -1,26 +0,0 @@
import Polymer from '../polymer';
export default new Polymer({
is: 'more-info-camera',
properties: {
stateObj: {
type: Object,
},
},
imageLoaded() {
this.fire('iron-resize');
},
computeCameraImageUrl(stateObj) {
if (__DEMO__) {
return '/demo/webcam.jpg';
} else if (stateObj) {
return `/api/camera_proxy_stream/${stateObj.entityId}` +
`?token=${stateObj.attributes.access_token}`;
}
// Return an empty image if no stateObj (= dialog not open)
return 'data:image/gif;base64,R0lGODlhAQABAAAAACw=';
},
});

View File

@ -1,7 +1,5 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
require('../components/loading-box');
export default new Polymer({ export default new Polymer({
is: 'more-info-configurator', is: 'more-info-configurator',

View File

@ -11,7 +11,6 @@ require('./more-info-thermostat');
require('./more-info-script'); require('./more-info-script');
require('./more-info-light'); require('./more-info-light');
require('./more-info-media_player'); require('./more-info-media_player');
require('./more-info-camera');
require('./more-info-updater'); require('./more-info-updater');
require('./more-info-alarm_control_panel'); require('./more-info-alarm_control_panel');
require('./more-info-lock'); require('./more-info-lock');

View File

@ -1,8 +1,6 @@
import Polymer from '../polymer'; import Polymer from '../polymer';
import attributeClassNames from '../util/attribute-class-names'; import attributeClassNames from '../util/attribute-class-names';
require('../components/ha-color-picker');
const ATTRIBUTE_CLASSES = ['brightness', 'rgb_color', 'color_temp']; const ATTRIBUTE_CLASSES = ['brightness', 'rgb_color', 'color_temp'];
function pickColor(hass, entityId, color) { function pickColor(hass, entityId, color) {

View File

@ -0,0 +1,36 @@
<script>
window.hassBehavior = {
attached: function attached() {
var hass = this.hass;
if (!hass) {
throw new Error('No hass property found on ' + this.nodeName);
}
this.nuclearUnwatchFns = Object.keys(this.properties).reduce(
function bindGetters(unwatchFns, key) {
if (!('bindNuclear' in this.properties[key])) {
return unwatchFns;
}
var getter = this.properties[key].bindNuclear(hass);
if (!getter) {
throw new Error(`Undefined getter specified for key ${key}`);
}
this[key] = hass.reactor.evaluate(getter);
return unwatchFns.concat(hass.reactor.observe(getter, function updateAttribute(val) {
this[key] = val;
}.bind(this)));
}.bind(this), []);
},
detached: function detached() {
while (this.nuclearUnwatchFns.length) {
this.nuclearUnwatchFns.shift()();
}
},
};
</script>

View File

@ -1,44 +0,0 @@
export default {
attached() {
const hass = this.hass;
if (!hass) {
throw new Error(`No hass property found on ${this.nodeName}`);
}
this.nuclearUnwatchFns = Object.keys(this.properties).reduce(
(unwatchFns, key) => {
if (!('bindNuclear' in this.properties[key])) {
return unwatchFns;
}
let getter = this.properties[key].bindNuclear;
if (typeof getter !== 'function') {
/* eslint-disable no-console */
console.warn(`Component ${this.nodeName} uses old style bindNuclear`);
/* eslint-enable no-console */
} else {
getter = getter(hass);
}
if (!getter) {
throw new Error(`Undefined getter specified for key ${key}`);
}
this[key] = hass.reactor.evaluate(getter);
return unwatchFns.concat(hass.reactor.observe(getter, (val) => {
this[key] = val;
}));
}, []);
},
detached() {
while (this.nuclearUnwatchFns.length) {
this.nuclearUnwatchFns.shift()();
}
},
};

View File

@ -1,7 +0,0 @@
export default function removeInitMessage() {
// remove the HTML init message
const initMsg = document.getElementById('ha-init-skeleton');
if (initMsg) {
initMsg.parentElement.removeChild(initMsg);
}
}

View File

@ -1,6 +0,0 @@
export default function (hass, authToken, rememberAuth) {
hass.authActions.validate(authToken, {
rememberAuth,
useStreaming: hass.localStoragePreferences.useStreaming,
});
}

View File

@ -9,6 +9,7 @@ var definePlugin = new webpack.DefinePlugin({
module.exports = { module.exports = {
entry: { entry: {
_app_compiled: './src/home-assistant.js', _app_compiled: './src/home-assistant.js',
_app_core_compiled: './src/app-core.js',
}, },
output: { output: {
path: 'build', path: 'build',