Initial getting rid of HA-JS (#180)

Getting rid of HA-JS
This commit is contained in:
Paulus Schoutsen 2017-01-29 18:34:45 -08:00 committed by GitHub
parent 2f71369dae
commit a1057681f1
70 changed files with 1965 additions and 1085 deletions

View File

@ -2,7 +2,9 @@
"extends": "airbnb-base",
"globals": {
"__DEV__": false,
"Polymer": true
"__DEMO__": false,
"Polymer": true,
"webkitSpeechRecognition": false,
},
"env": {
"browser": true
@ -19,7 +21,10 @@
"prefer-spread": 0,
"no-plusplus": 0,
"no-bitwise": 0,
"comma-dangle": 0
"comma-dangle": 0,
"vars-on-top": 0,
"no-continue": 0,
"no-param-reassign": 0
},
plugins: [
"html"

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "home-assistant-js"]
path = home-assistant-js
url = https://github.com/home-assistant/home-assistant-js.git

@ -1 +0,0 @@
Subproject commit e5b4d6e5a3d1348c82a581dc728709a44a5fcc4a

View File

@ -30,11 +30,7 @@
"license": "Apache-2.0",
"_depComment": "keymirror, nuclear-js, object-assign, ha-js-ws are for ha-js",
"dependencies": {
"classnames": "^2.2.5",
"home-assistant-js-websocket": "0.5.0",
"keymirror": "^0.1.1",
"nuclear-js": "^1.4.0",
"object-assign": "^4.1.1"
"home-assistant-js-websocket": "^0.7.3"
},
"devDependencies": {
"bower": "^1.8.0",

View File

@ -1,6 +1,5 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<dom-module id="events-list">
<template>
@ -24,7 +23,7 @@
<template is='dom-repeat' items='[[events]]' as='event'>
<li>
<a href='#' on-click='eventSelected'>{{event.event}}</a>
<span> (</span><span>{{event.listenerCount}}</span><span> listeners)</span>
<span> (</span><span>{{event.listener_count}}</span><span> listeners)</span>
</li>
</template>
</ul>
@ -35,8 +34,6 @@
Polymer({
is: 'events-list',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -44,17 +41,15 @@
events: {
type: Array,
bindNuclear: function (hass) {
return [
hass.eventGetters.entityMap,
function (map) {
return map.valueSeq().sortBy(function (event) { return event.event; }).toArray();
},
];
},
},
},
attached: function () {
this.hass.callApi('GET', 'events').then(function (events) {
this.events = events;
}.bind(this));
},
eventSelected: function (ev) {
ev.preventDefault();
this.fire('event-selected', { eventType: ev.model.event.event });

View File

@ -116,7 +116,12 @@ Polymer({
return;
}
this.hass.eventActions.fireEvent(this.eventType, eventData);
this.hass.callApi('POST', 'events/' + this.eventType, eventData)
.then(function () {
this.fire('hass-notification', {
message: 'Event ' + this.eventType + ' successful fired!',
});
}.bind(this));
},
computeFormClasses: function (narrow) {

View File

@ -7,7 +7,6 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<link rel="import" href="../../src/resources/ha-style.html">
<dom-module id="ha-panel-dev-info">
@ -71,10 +70,10 @@
<p class='version'>
<a href='https://home-assistant.io'><img src="/static/icons/favicon-192x192.png" height="192" /></a><br />
Home Assistant<br />
[[hassVersion]]
[[hass.config.core.version]]
</p>
<p>
Path to configuration.yaml: [[hassConfigDir]]
Path to configuration.yaml: [[hass.config.core.config_dir]]
</p>
<p class='develop'>
<a href='https://home-assistant.io/developers/credits/' target='_blank'>
@ -92,7 +91,6 @@
Built using
<a href='https://www.python.org'>Python 3</a>,
<a href='https://www.polymer-project.org' target='_blank'>Polymer [[polymerVersion]]</a>,
<a href='https://optimizely.github.io/nuclear-js/' target='_blank'>NuclearJS [[nuclearVersion]]</a><br />
Icons by <a href='https://www.google.com/design/icons/' target='_blank'>Google</a> and <a href='https://MaterialDesignIcons.com' target='_blank'>MaterialDesignIcons.com</a>.
</p>
</div>
@ -110,8 +108,6 @@
Polymer({
is: 'ha-panel-dev-info',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -127,30 +123,11 @@ Polymer({
value: false,
},
hassVersion: {
type: String,
bindNuclear: function (hass) {
return hass.configGetters.serverVersion;
},
},
hassConfigDir: {
type: String,
bindNuclear: function (hass) {
return hass.configGetters.configDir;
},
},
polymerVersion: {
type: String,
value: Polymer.version,
},
nuclearVersion: {
type: String,
value: '1.4.0',
},
errorLog: {
type: String,
value: '',
@ -166,7 +143,7 @@ Polymer({
this.errorLog = 'Loading error log…';
this.hass.errorLogActions.fetchErrorLog().then(
this.hass.callApi('GET', 'error_log').then(
function (log) {
this.errorLog = log || 'No errors have been reported.';
}.bind(this));

View File

@ -12,7 +12,6 @@
<link rel='import' href='../../src/components/ha-menu-button.html'>
<link rel='import' href='../../src/resources/ha-style.html'>
<link rel='import' href='../../src/util/hass-behavior.html'>
<dom-module id='ha-panel-dev-service'>
<template>
@ -142,8 +141,6 @@
Polymer({
is: 'ha-panel-dev-service',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -178,56 +175,37 @@ Polymer({
_attributes: {
type: Array,
computed: 'computeAttributesArray(hass, domain, service)',
computed: 'computeAttributesArray(serviceDomains, domain, service)',
},
serviceDomains: {
type: Array,
bindNuclear: function (hass) {
return hass.serviceGetters.entityMap;
},
type: Object,
computed: 'computeServiceDomains(hass)',
},
},
computeAttributesArray: function (hass, domain, service) {
return hass.reactor.evaluate([
hass.serviceGetters.entityMap,
function (map) {
if (map.has(domain) && map.get(domain).get('services').has(service)) {
return map
.get(domain)
.get('services')
.get(service)
.get('fields')
.map(function (field, key) {
var fieldCopy = field.toJS();
fieldCopy.key = key;
return fieldCopy;
})
.toArray();
}
return [];
},
]);
computeServiceDomains: function (hass) {
return hass.config.services;
},
computeAttributesArray: function (serviceDomains, domain, service) {
if (!(domain in serviceDomains)) return [];
if (!(service in serviceDomains[domain])) return [];
var fields = serviceDomains[domain][service].fields;
return Object.keys(fields).map(function (field) {
return Object.assign({}, fields[field], { key: field });
});
},
computeDomains: function (serviceDomains) {
return serviceDomains
.valueSeq()
.map(function (domain) { return domain.domain; })
.sort()
.toJS();
return Object.keys(serviceDomains).sort();
},
computeServices: function (serviceDomains, domain) {
if (domain) {
return serviceDomains
.get(domain)
.get('services')
.keySeq()
.toArray();
}
return '';
if (!(domain in serviceDomains)) return [];
return Object.keys(serviceDomains[domain]).sort();
},
domainChanged: function () {
@ -250,7 +228,7 @@ Polymer({
return;
}
this.hass.serviceActions.callService(this.domain, this.service, serviceData);
this.hass.callService(this.domain, this.service, serviceData);
},
});
</script>

View File

@ -10,7 +10,6 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<link rel="import" href="../../src/resources/ha-style.html">
<dom-module id="ha-panel-dev-state">
@ -82,7 +81,7 @@
</tr>
<template is='dom-repeat' items='[[_entities]]' as='entity'>
<tr>
<td><a href='#' on-tap='entitySelected'>[[entity.entityId]]</a></td>
<td><a href='#' on-tap='entitySelected'>[[entity.entity_id]]</a></td>
<td>[[entity.state]]</td>
<template is='dom-if' if='[[computeShowAttributes(narrow, _showAttributes)]]'>
<td>[[attributeString(entity)]]</td>
@ -99,8 +98,6 @@
Polymer({
is: 'ha-panel-dev-state',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -138,20 +135,13 @@ Polymer({
_entities: {
type: Array,
bindNuclear: function (hass) {
return [
hass.entityGetters.entityMap,
function (map) {
return map.valueSeq().sortBy(function (entity) { return entity.entityId; }).toArray();
},
];
},
computed: 'computeEntities(hass)',
},
},
entitySelected: function (ev) {
var state = ev.model.entity;
this._entityId = state.entityId;
this._entityId = state.entity_id;
this._state = state.state;
this._stateAttributes = JSON.stringify(state.attributes, null, ' ');
ev.preventDefault();
@ -168,13 +158,25 @@ Polymer({
return;
}
this.hass.entityActions.save({
entityId: this._entityId,
this.hass.callApi('POST', 'states/' + this._entityId, {
state: this._state,
attributes: attr,
});
},
computeEntities: function (hass) {
return Object.keys(hass.states).map(function (key) { return hass.states[key]; })
.sort(function (entityA, entityB) {
if (entityA.entity_id < entityB.entity_id) {
return -1;
}
if (entityB.entity_id > entityA.entity_id) {
return 1;
}
return 0;
});
},
computeShowAttributes: function (narrow, _showAttributes) {
return !narrow && _showAttributes;
},

View File

@ -8,7 +8,6 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<link rel="import" href="../../src/resources/ha-style.html">
<dom-module id="ha-panel-dev-template">
@ -93,8 +92,6 @@
Polymer({
is: 'ha-panel-dev-template',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -170,14 +167,15 @@ Polymer({
renderTemplate: function () {
this.rendering = true;
this.hass.templateActions.render(this.template).then(function (processed) {
this.processed = processed;
this.rendering = false;
}.bind(this), function (error) {
this.processed = error.message;
this.error = true;
this.rendering = false;
}.bind(this));
this.hass.callApi('POST', 'template', { template: this.template })
.then(function (processed) {
this.processed = processed;
this.rendering = false;
}.bind(this), function (error) {
this.processed = error.message;
this.error = true;
this.rendering = false;
}.bind(this));
},
});
</script>

View File

@ -9,7 +9,7 @@
<link rel="import" href="../../src/components/state-history-charts.html">
<link rel="import" href="../../src/resources/pikaday-js.html">
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<link rel="import" href="../../src/data/ha-state-history-data.html">
<link rel="import" href="../../src/resources/ha-style.html">
<dom-module id="ha-panel-history">
@ -24,15 +24,19 @@
}
</style>
<ha-state-history-data
hass='[[hass]]'
filter-type='[[_filterType]]'
filter-value='[[_computeFilterDate(_selectedDate)]]'
data='{{stateHistory}}'
isLoading='{{isLoadingData}}'
></ha-state-history-data>
<app-header-layout has-scrolling-region>
<app-header fixed>
<app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>History</div>
<paper-icon-button
icon="mdi:refresh"
on-tap="handleRefreshClick"
></paper-icon-button>
</app-toolbar>
</app-header>
@ -40,11 +44,11 @@
<paper-input
label='Showing entries for'
id='datePicker'
value='[[selectedDateStr]]'
value='[[_computeDateDisplay(_selectedDate)]]'
on-focus='datepickerFocus'
></paper-input>
<state-history-charts state-history="[[stateHistory]]"
<state-history-charts history-data="[[stateHistory]]"
is-loading-data="[[isLoadingData]]"></state-history-charts>
</div>
</app-header-layout>
@ -55,8 +59,6 @@
Polymer({
is: 'ha-panel-history',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -71,61 +73,27 @@ Polymer({
value: false,
},
isDataLoaded: {
type: Boolean,
bindNuclear: function (hass) {
return hass.entityHistoryGetters.hasDataForCurrentDate;
},
observer: 'isDataLoadedChanged',
},
stateHistory: {
type: Object,
bindNuclear: function (hass) {
return hass.entityHistoryGetters.entityHistoryForCurrentDate;
},
value: null,
},
isLoadingData: {
type: Boolean,
bindNuclear: function (hass) {
return hass.entityHistoryGetters.isLoadingEntityHistory;
value: false,
},
_selectedDate: {
type: Date,
value: function () {
return new Date();
},
},
selectedDate: {
_filterType: {
type: String,
value: null,
bindNuclear: function (hass) {
return hass.entityHistoryGetters.currentDate;
},
value: 'date',
},
selectedDateStr: {
type: String,
value: null,
bindNuclear: function (hass) {
return [
hass.entityHistoryGetters.currentDate,
function (currentDate) {
var dateObj = new Date(currentDate);
return window.hassUtil.formatDate(dateObj);
},
];
},
},
},
isDataLoadedChanged: function (newVal) {
if (!newVal) {
this.async(function () {
this.hass.entityHistoryActions.fetchSelectedDate();
}.bind(this), 1);
}
},
handleRefreshClick: function () {
this.hass.entityHistoryActions.fetchSelectedDate();
},
datepickerFocus: function () {
@ -138,15 +106,30 @@ Polymer({
// field value with its internal formatting.
field: document.createElement('input'),
trigger: this.$.datePicker.inputElement,
onSelect: this.hass.entityHistoryActions.changeCurrentDate,
onSelect: function (newDate) {
newDate.setDate(newDate.getDate() + 1);
if (newDate > new Date()) {
newDate = new Date();
}
this._selectedDate = newDate;
}.bind(this),
});
// Set the initial datePicker date, without triggering onSelect handler.
this.datePicker.setDate(this.selectedDate, true);
this.datePicker.setDate(this._selectedDate, true);
},
detached: function () {
this.datePicker.destroy();
},
_computeDateDisplay: function (date) {
return window.hassUtil.formatDate(new Date(date));
},
_computeFilterDate: function (_selectedDate) {
return _selectedDate.toISOString();
},
});
</script>

View File

@ -0,0 +1,70 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<script>
(function () {
var DATE_CACHE = {};
Polymer({
is: 'ha-logbook-data',
properties: {
hass: {
type: Object,
observer: 'hassChanged',
},
filterDate: {
type: String,
observer: 'filterDateChanged',
},
isLoading: {
type: Boolean,
value: true,
readOnly: true,
notify: true,
},
entries: {
type: Object,
value: null,
readOnly: true,
notify: true,
},
},
hassChanged: function (newHass, oldHass) {
if (!oldHass && this.filterDate) {
this.filterDateChanged(this.filterDate);
}
},
filterDateChanged: function (filterDate) {
if (!this.hass) return;
this._setIsLoading(true);
this.getDate(filterDate).then(function (logbookEntries) {
this._setEntries(logbookEntries);
this._setIsLoading(false);
}.bind(this));
},
getDate: function (date) {
if (!DATE_CACHE[date]) {
DATE_CACHE[date] = this.hass.callApi('GET', 'logbook/' + date).then(
function (logbookEntries) {
return logbookEntries;
},
function () {
DATE_CACHE[date] = false;
return null;
}
);
}
return DATE_CACHE[date];
},
});
}());
</script>

View File

@ -47,10 +47,10 @@
<div class='time'>[[formatTime(item.when)]]</div>
<domain-icon domain="[[item.domain]]" class='icon'></domain-icon>
<div class='message' flex>
<template is='dom-if' if="[[!item.entityId]]">
<template is='dom-if' if="[[!item.entity_id]]">
<span class='name'>[[item.name]]</span>
</template>
<template is='dom-if' if="[[item.entityId]]">
<template is='dom-if' if="[[item.entity_id]]">
<a href='#' on-tap="entityClicked" class='name'>[[item.name]]</a>
</template>
<span> </span>
@ -76,13 +76,13 @@ Polymer({
},
},
formatTime: function (dateObj) {
return window.hassUtil.formatTime(dateObj);
formatTime: function (date) {
return window.hassUtil.formatTime(new Date(date));
},
entityClicked: function (ev) {
ev.preventDefault();
this.hass.moreInfoActions.selectEntity(ev.model.item.entityId);
this.fire('hass-more-info', { entityId: ev.model.item.entity_id });
},
});
</script>

View File

@ -10,10 +10,10 @@
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/resources/pikaday-js.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<link rel="import" href="../../src/resources/ha-style.html">
<link rel="import" href="./ha-logbook.html">
<link rel="import" href="./ha-logbook-data.html">
<dom-module id="ha-panel-logbook">
<template>
@ -31,15 +31,18 @@
}
</style>
<ha-logbook-data
hass='[[hass]]'
is-loading='{{isLoading}}'
entries='{{entries}}'
filter-date='[[_computeFilterDate(_selectedDate)]]'
></ha-logbook-data>
<app-header-layout has-scrolling-region>
<app-header fixed>
<app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>Logbook</div>
<paper-icon-button
icon="mdi:refresh"
on-tap="handleRefresh"
></paper-icon-button>
</app-toolbar>
</app-header>
@ -48,7 +51,7 @@
<paper-input
label='Showing entries for'
id='datePicker'
value='[[selectedDateStr]]'
value='[[_computeDateDisplay(_selectedDate)]]'
on-focus='datepickerFocus'
></paper-input>
@ -68,8 +71,6 @@
Polymer({
is: 'ha-panel-logbook',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -85,50 +86,19 @@ Polymer({
value: false,
},
selectedDate: {
_selectedDate: {
type: String,
bindNuclear: function (hass) {
return hass.logbookGetters.currentDate;
},
},
selectedDateStr: {
type: String,
value: null,
bindNuclear: function (hass) {
return [
hass.logbookGetters.currentDate,
function (currentDate) {
var dateObj = new Date(currentDate);
return window.hassUtil.formatDate(dateObj);
},
];
value: function () {
return new Date();
},
},
isLoading: {
type: Boolean,
bindNuclear: function (hass) {
return hass.logbookGetters.isLoadingEntries;
},
},
isStale: {
type: Boolean,
bindNuclear: function (hass) {
return hass.logbookGetters.isCurrentStale;
},
observer: 'isStaleChanged',
},
entries: {
type: Array,
bindNuclear: function (hass) {
return [
hass.logbookGetters.currentEntries,
function (entries) { return entries.reverse().toArray(); },
];
},
},
datePicker: {
@ -136,18 +106,6 @@ Polymer({
},
},
isStaleChanged: function (newVal) {
if (newVal) {
this.async(function () {
this.hass.logbookActions.fetchDate(this.selectedDate);
}.bind(this), 1);
}
},
handleRefresh: function () {
this.hass.logbookActions.fetchDate(this.selectedDate);
},
datepickerFocus: function () {
this.datePicker.adjustPosition();
},
@ -158,7 +116,15 @@ Polymer({
// field value with its internal formatting.
field: document.createElement('input'),
trigger: this.$.datePicker.inputElement,
onSelect: this.hass.logbookActions.changeCurrentDate,
onSelect: function (newDate) {
newDate.setDate(newDate.getDate() + 1);
if (newDate > new Date()) {
newDate = new Date();
}
this._selectedDate = newDate;
}.bind(this),
});
// Set the initial datePicker date, without triggering onSelect handler.
this.datePicker.setDate(this.selectedDate, true);
@ -168,5 +134,12 @@ Polymer({
this.datePicker.destroy();
},
_computeDateDisplay: function (date) {
return window.hassUtil.formatDate(new Date(date));
},
_computeFilterDate: function (_selectedDate) {
return _selectedDate.toISOString();
},
});
</script>

View File

@ -27,27 +27,24 @@
</style>
<div class='marker'>
<template is='dom-if' if='[[icon]]'>
<iron-icon icon='[[icon]]'></iron-icon>
</template>
<template is='dom-if' if='[[value]]'>[[value]]</template>
<template is='dom-if' if='[[image]]'>
<iron-image sizing='cover' class='fit' src='[[image]]'></iron-image>
<template is='dom-if' if='[[entityName]]'>[[entityName]]</template>
<template is='dom-if' if='[[entityPicture]]'>
<iron-image sizing='cover' class='fit' src='[[entityPicture]]'></iron-image>
</template>
</div>
</template>
</dom-module>
<script>
/*
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.
That's why we refer here to window.hass instead of the hass property.
*/
Polymer({
is: 'ha-entity-marker',
hostAttributes: {
entityId: null,
entityName: null,
entityPicture: null,
},
properties: {
hass: {
type: Object,
@ -56,28 +53,17 @@ Polymer({
entityId: {
type: String,
value: '',
reflectToAttribute: true,
},
state: {
type: Object,
computed: 'computeState(entityId)',
},
icon: {
type: Object,
computed: 'computeIcon(state)',
},
image: {
type: Object,
computed: 'computeImage(state)',
},
value: {
entityName: {
type: String,
computed: 'computeValue(state)',
value: null,
},
entityPicture: {
type: String,
value: null,
}
},
listeners: {
@ -87,29 +73,8 @@ Polymer({
badgeTap: function (ev) {
ev.stopPropagation();
if (this.entityId) {
this.async(function () {
window.hass.moreInfoActions.selectEntity(this.entityId);
}, 1);
this.fire('hass-more-info', { entityId: this.entityId });
}
},
computeState: function (entityId) {
return entityId && window.hass.reactor.evaluate(window.hass.entityGetters.byId(entityId));
},
computeIcon: function (state) {
return !state && 'home';
},
computeImage: function (state) {
return state && state.attributes.entity_picture;
},
computeValue: function (state) {
return state &&
state.entityDisplay.split(' ').map(function (part) {
return part.substr(0, 1);
}).join('');
},
});
</script>

View File

@ -8,7 +8,6 @@
<link rel="stylesheet" href="../../bower_components/leaflet/dist/leaflet.css" />
<link rel="import" href="../../src/components/ha-menu-button.html">
<link rel="import" href="../../src/util/hass-behavior.html">
<link rel="import" href="./ha-entity-marker.html">
@ -37,41 +36,9 @@ window.L.Icon.Default.imagePath = '/static/images/leaflet';
Polymer({
is: 'ha-panel-map',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
locationGPS: {
type: Number,
bindNuclear: function (hass) {
return hass.configGetters.locationGPS;
},
},
locationName: {
type: String,
bindNuclear: function (hass) {
return hass.configGetters.locationName;
},
},
locationEntities: {
type: Array,
bindNuclear: function (hass) {
return [
hass.entityGetters.entityMap,
function (entities) {
return entities.valueSeq().filter(
function (entity) {
return 'latitude' in entity.attributes;
}
);
},
];
},
observer: 'drawEntities',
},
@ -96,7 +63,7 @@ Polymer({
}
).addTo(map);
this.drawEntities(this.locationEntities);
this.drawEntities(this.hass);
this.async(function () {
map.invalidateSize();
@ -118,7 +85,7 @@ Polymer({
this._map.fitBounds(bounds.pad(0.5));
},
drawEntities: function (entities) {
drawEntities: function (hass) {
/* eslint-disable vars-on-top */
var map = this._map;
if (!map) return;
@ -128,10 +95,21 @@ Polymer({
}
var mapItems = this._mapItems = [];
entities.forEach(function (entity) {
Object.keys(hass.states).forEach(function (entityId) {
var entity = hass.states[entityId];
var title = window.hassUtil.computeStateName(entity);
if ((entity.attributes.hidden &&
window.hassUtil.computeDomain(entity) !== 'zone') ||
entity.state === 'home' ||
!('latitude' in entity.attributes) ||
!('longitude' in entity.attributes)) {
return;
}
var icon;
if (entity.domain === 'zone') {
if (window.hassUtil.computeDomain(entity) === 'zone') {
// DRAW ZONE
if (entity.attributes.passive) return;
@ -141,7 +119,7 @@ Polymer({
iconHTML = (
"<iron-icon icon='" + entity.attributes.icon + "'></iron-icon>");
} else {
iconHTML = entity.entityDisplay;
iconHTML = title;
}
icon = window.L.divIcon({
@ -156,7 +134,7 @@ Polymer({
{
icon,
interactive: false,
title: entity.entityDisplay,
title: title,
}
).addTo(map));
@ -173,13 +151,14 @@ Polymer({
return;
}
// Filter out entities at home
if (entity.state === 'home' || entity.attributes.hidden) return;
// DRAW ENTITY
// create icon
var entityPicture = entity.attributes.entity_picture || '';
var entityName = title.split(' ').map(function (part) { return part.substr(0, 1); }).join('');
/* Leaflet clones this element before adding it to the map. This messes up
our Poylmer object and we can't pass data through. Thus we hack like this. */
icon = window.L.divIcon({
html: "<ha-entity-marker entity-id='" + entity.entityId + "'></ha-entity-marker>",
html: "<ha-entity-marker entity-id='" + entity.entity_id + "' entity-name='" + entityName + "' entity-picture='" + entityPicture + "'></ha-entity-marker>",
iconSize: [45, 45],
className: '',
});
@ -189,7 +168,7 @@ Polymer({
[entity.attributes.latitude, entity.attributes.longitude],
{
icon,
title: entity.entityDisplay,
title: window.hassUtil.computeStateName(entity),
}
).addTo(map));
@ -212,7 +191,7 @@ Polymer({
},
toggleMenu: function () {
this.fire('open-menu');
this.fire('hass-open-menu');
},
});
</script>

View File

@ -18,11 +18,10 @@ const plugins = [
__DEMO__: JSON.stringify(DEMO),
},
}),
buble(),
];
if (!DEV) {
plugins.push(buble());
plugins.push(uglify());
}

View File

@ -1,41 +1,28 @@
import HomeAssistant from '../home-assistant-js/src/index';
import * as HAWS from 'home-assistant-js-websocket';
const hass = new HomeAssistant();
window.HAWS = HAWS;
window.HASS_DEMO = __DEMO__;
window.validateAuth = function validateAuth(authToken, rememberAuth) {
hass.authActions.validate(authToken, {
rememberAuth,
useStreaming: hass.localStoragePreferences.useStreaming,
});
const init = window.createHassConnection = function (password) {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
const url = `${proto}://${window.location.host}/api/websocket`;
const options = {};
if (password !== undefined) {
options.authToken = password;
}
return HAWS.createConnection(url, options)
.then(function (conn) {
HAWS.subscribeEntities(conn);
HAWS.subscribeConfig(conn);
return conn;
});
};
window.removeInitMsg = function removeInitMessage() {
// remove the HTML init message
const initMsg = document.getElementById('ha-init-skeleton');
if (initMsg) {
initMsg.parentElement.removeChild(initMsg);
}
};
hass.reactor.batch(function () {
hass.navigationActions.showSidebar(
hass.localStoragePreferences.showSidebar);
// if auth was given, tell the backend
if (window.noAuth) {
window.validateAuth('', false);
} else if (hass.localStoragePreferences.authToken) {
window.validateAuth(hass.localStoragePreferences.authToken, true);
}
});
setTimeout(hass.startLocalStoragePreferencesSync, 5000);
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/service_worker.js');
});
if (window.noAuth) {
window.hassConnection = init();
} else if (window.localStorage.authToken) {
window.hassConnection = init(window.localStorage.authToken);
} else {
window.hassConnection = null;
}
// While we figure out how ha-entity-marker can keep it's references
window.hass = hass;

View File

@ -38,9 +38,9 @@
</style>
<img src='[[cameraFeedSrc]]' class='camera-feed' hidden$='[[!imageLoaded]]'
on-load='imageLoadSuccess' on-error='imageLoadFail' alt='[[stateObj.entityDisplay]]'>
on-load='imageLoadSuccess' on-error='imageLoadFail' alt='[[computeStateName(stateObj)]]'>
<div class='caption'>
[[stateObj.entityDisplay]]
[[computeStateName(stateObj)]]
<template is='dom-if' if='[[!imageLoaded]]'>
(Error loading image)
</template>
@ -99,9 +99,7 @@ Polymer({
},
cardTapped: function () {
this.async(function () {
this.hass.moreInfoActions.selectEntity(this.stateObj.entityId);
}.bind(this), 1);
this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
},
updateCameraFeedSrc: function (stateObj) {
@ -117,5 +115,9 @@ Polymer({
imageLoadFail: function () {
this.imageLoaded = false;
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
},
});
</script>

View File

@ -75,14 +75,16 @@ Polymer({
states: {
type: Array,
},
groupEntity: {
type: Object,
},
},
computeTitle: function (states, groupEntity) {
return groupEntity ? groupEntity.entityDisplay :
states[0].domain.replace(/_/g, ' ');
return groupEntity ?
window.hassUtil.computeStateName(groupEntity) :
window.hassUtil.computeDomain(states[0]).replace(/_/g, ' ');
},
computeTitleClass: function (groupEntity) {
@ -105,26 +107,32 @@ Polymer({
ev.stopPropagation();
if (ev.model) {
entityId = ev.model.item.entityId;
entityId = ev.model.item.entity_id;
} else {
entityId = this.groupEntity.entityId;
entityId = this.groupEntity.entity_id;
}
this.async(function () { this.hass.moreInfoActions.selectEntity(entityId); }.bind(this), 1);
this.fire('hass-more-info', { entityId: entityId });
},
showGroupToggle: function (groupEntity, states) {
var canToggleCount;
if (!groupEntity || !states || groupEntity.attributes.control === 'hidden' ||
(groupEntity.state !== 'on' && groupEntity.state !== 'off')) {
return false;
}
// only show if we can toggle 2+ entities in group
canToggleCount = states.reduce(
function (sum, state) {
return sum + window.hassUtil.canToggle(this.hass, state.entityId);
}, 0);
var canToggleCount = 0;
for (var i = 0; i < states.length; i++) {
if (!window.hassUtil.canToggleState(this.hass, states[i])) {
continue;
}
canToggleCount++;
if (canToggleCount > 1) {
break;
}
}
return canToggleCount > 1;
},

View File

@ -6,6 +6,8 @@
<link rel='import' href='../../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel='import' href='../../bower_components/paper-progress/paper-progress.html'>
<link rel='import' href='../util/media-player-model.html'>
<dom-module id='ha-media_player-card'>
<template>
<style include="paper-material iron-flex iron-flex-alignment iron-positioning">
@ -149,7 +151,7 @@
<div class='cover' id='cover'></div>
<div class='caption'>
[[stateObj.entityDisplay]]
[[computeStateName(stateObj)]]
<div class='title'>[[playerObj.primaryText]]</div>
[[playerObj.secondaryText]]<br />
</div>
@ -217,7 +219,7 @@ Polymer({
playerObj: {
type: Object,
computed: 'computePlayerObj(stateObj)',
computed: 'computePlayerObj(hass, stateObj)',
observer: 'playerObjChanged',
},
@ -290,8 +292,8 @@ Polymer({
return playerObj.isOff ? !playerObj.supportsTurnOn : !playerObj.supportsTurnOff;
},
computePlayerObj: function (stateObj) {
return stateObj.domainModel(this.hass);
computePlayerObj: function (hass, stateObj) {
return new window.MediaPlayerEntity(hass, stateObj);
},
computePlaybackControlIcon: function (playerObj) {
@ -303,6 +305,10 @@ Polymer({
return '';
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
},
handleNext: function (ev) {
ev.stopPropagation();
this.playerObj.nextTrack();
@ -310,9 +316,7 @@ Polymer({
handleOpenMoreInfo: function (ev) {
ev.stopPropagation();
this.async(function () {
this.hass.moreInfoActions.selectEntity(this.stateObj.entityId);
}, 1);
this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
},
handlePlaybackControl: function (ev) {

View File

@ -49,7 +49,8 @@ Polymer({
],
computeTitle: function (stateObj) {
return stateObj.attributes.title || stateObj.entityDisplay;
return (stateObj.attributes.title ||
window.hassUtil.computeStateName(stateObj));
},
loadScript: function () {
@ -86,7 +87,7 @@ Polymer({
dismissTap: function (ev) {
ev.preventDefault();
this.hass.entityActions.delete(this.stateObj);
this.hass.callApi('DELETE', 'states/' + this.stateObj.entity_id);
},
});
</script>

View File

@ -110,24 +110,26 @@ Polymer({
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
callService: function (turnOn) {
var domain;
var stateDomain = window.hassUtil.computeDomain(this.stateObj);
var serviceDomain;
var service;
var currentState;
if (this.stateObj.domain === 'lock') {
domain = 'lock';
if (stateDomain === 'lock') {
serviceDomain = 'lock';
service = turnOn ? 'lock' : 'unlock';
} else if (this.stateObj.domain === 'garage_door') {
domain = 'garage_door';
} else if (stateDomain === 'cover') {
serviceDomain = 'cover';
service = turnOn ? 'open' : 'close';
} else {
domain = 'homeassistant';
serviceDomain = 'homeassistant';
service = turnOn ? 'turn_on' : 'turn_off';
}
currentState = this.stateObj;
this.hass.serviceActions.callService(domain, service,
{ entity_id: this.stateObj.entityId })
this.hass.callService(
serviceDomain, service,
{ entity_id: this.stateObj.entity_id })
.then(function () {
setTimeout(function () {
// If after 2 seconds we have not received a state update

View File

@ -57,13 +57,11 @@ Polymer({
badgeTap: function (ev) {
ev.stopPropagation();
this.async(function () {
this.hass.moreInfoActions.selectEntity(this.state.entityId);
}, 1);
this.fire('hass-more-info', { entityId: this.state.entity_id });
},
computeClasses: function (state) {
switch (state.domain) {
switch (window.hassUtil.computeDomain(state)) {
case 'binary_sensor':
case 'updater':
return 'blue';
@ -73,7 +71,7 @@ Polymer({
},
computeValue: function (state) {
switch (state.domain) {
switch (window.hassUtil.computeDomain(state)) {
case 'binary_sensor':
case 'device_tracker':
case 'updater':
@ -90,7 +88,7 @@ Polymer({
if (state.state === 'unavailable') {
return null;
}
switch (state.domain) {
switch (window.hassUtil.computeDomain(state)) {
case 'alarm_control_panel':
if (state.state === 'pending') {
return 'mdi:clock-fast';
@ -123,7 +121,7 @@ Polymer({
if (state.state === 'unavailable') {
return 'unavai';
}
switch (state.domain) {
switch (window.hassUtil.computeDomain(state)) {
case 'device_tracker':
return state.state === 'not_home' ? 'Away' : state.state;
case 'alarm_control_panel':
@ -142,7 +140,7 @@ Polymer({
},
computeDescription: function (state) {
return state.entityDisplay;
return window.hassUtil.computeStateName(state);
},
stateChanged: function () {

View File

@ -38,7 +38,7 @@
<ha-state-icon
id='icon'
state-obj='[[stateObj]]'
data-domain$='[[stateObj.domain]]'
data-domain$='[[computeDomain(stateObj)]]'
data-state$='[[stateObj.state]]'
></ha-state-icon>
</template>
@ -55,6 +55,10 @@ Polymer({
},
},
computeDomain: function (stateObj) {
return window.hassUtil.computeDomain(stateObj);
},
/**
* Called when an attribute changes that influences the color of the icon.
*/
@ -71,9 +75,10 @@ Polymer({
// for domain light, set color of icon to light color if available and it is
// not very white (sum rgb colors < 730)
if (newVal.domain === 'light' && newVal.state === 'on' &&
newVal.attributes.rgb_color &&
newVal.attributes.rgb_color.reduce(function (cur, tot) { return cur + tot; }, 0) < 730) {
if (window.hassUtil.computeDomain(newVal) === 'light' &&
newVal.state === 'on' &&
newVal.attributes.rgb_color &&
newVal.attributes.rgb_color.reduce(function (cur, tot) { return cur + tot; }, 0) < 730) {
this.$.icon.style.color = 'rgb(' + newVal.attributes.rgb_color.join(',') + ')';
} else {
this.$.icon.style.color = null;

View File

@ -40,7 +40,7 @@
<state-badge state-obj='[[stateObj]]'></state-badge>
<div class='info'>
<div class='name' in-dialog$='[[inDialog]]'>[[stateObj.entityDisplay]]</div>
<div class='name' in-dialog$='[[inDialog]]'>[[computeStateName(stateObj)]]</div>
<template is='dom-if' if='[[inDialog]]'>
<div class='time-ago'>
@ -71,5 +71,9 @@ Polymer({
type: Boolean,
},
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
}
});
</script>

View File

@ -88,11 +88,18 @@
weather: 4,
};
// 4 types:
// badges: 0 .. 10
// before groups < 0
// groups: X
// rest: 100
var PRIORITY = {
// before groups < 0
configurator: -20,
persistent_notification: -15,
group: -10,
a: -1,
// badges have priority >= 0
updater: 0,
sun: 1,
device_tracker: 2,
@ -102,14 +109,40 @@
};
function getPriority(domain) {
return (domain in PRIORITY) ? PRIORITY[domain] : 30;
return (domain in PRIORITY) ? PRIORITY[domain] : 100;
}
function entitySortBy(entity) {
return entity.domain === 'group' ? entity.attributes.order :
entity.entityDisplay.toLowerCase();
function sortPriority(domainA, domainB) {
return domainA.priority - domainB.priority;
}
function entitySortBy(entityA, entityB) {
var nameA = (entityA.attributes.friendly_name ||
entityA.entity_id).toLowerCase();
var nameB = (entityB.attributes.friendly_name ||
entityB.entity_id).toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameB > nameA) {
return 1;
}
return 0;
}
function iterateDomainSorted(collection, func) {
Object.keys(collection)
.map(function (key) { return collection[key]; })
.sort(sortPriority)
.forEach(function (domain) {
domain.states.sort(entitySortBy);
func(domain);
});
}
var computeDomain = window.hassUtil.computeDomain;
Polymer({
is: 'ha-cards',
@ -138,6 +171,7 @@
viewVisible: {
type: Boolean,
value: false,
},
cards: {
@ -160,13 +194,11 @@
if (this.panelVisible && this.viewVisible) {
this.cards = this.computeCards(columns, states, showIntroduction);
}
}.bind(this));
}.bind(this), 10);
},
computeCards: function (columns, states, showIntroduction) {
var hass = this.hass;
var byDomain = states.groupBy(function (entity) { return entity.domain; });
var hasGroup = {};
var cards = {
demo: false,
@ -174,17 +206,12 @@
columns: [],
};
var entityCount = [];
var expandGroup;
var i;
for (i = 0; i < columns; i++) {
cards.columns.push([]);
entityCount.push(0);
}
function filterGrouped(entities) {
return entities.filter(function (entity) { return !(entity.entityId in hasGroup); });
}
// Find column with < 5 entities, else column with lowest count
function getIndex(size) {
var minIndex = 0;
@ -206,7 +233,7 @@
cards.columns[getIndex(5)].push({
hass: hass,
cardType: 'introduction',
showHideInstruction: states.size > 0 && !hass.demo,
showHideInstruction: states.size > 0 && !window.HASS_DEMO,
});
}
@ -223,9 +250,11 @@
size = 0;
entities.forEach(function (entity) {
if (entity.domain in DOMAINS_WITH_CARD) {
var domain = computeDomain(entity);
if (domain in DOMAINS_WITH_CARD) {
owncard.push(entity);
size += DOMAINS_WITH_CARD[entity.domain];
size += DOMAINS_WITH_CARD[domain];
} else {
other.push(entity);
size++;
@ -249,43 +278,71 @@
owncard.forEach(function (entity) {
cards.columns[curIndex].push({
hass: hass,
cardType: entity.domain,
cardType: computeDomain(entity),
stateObj: entity,
});
});
}
expandGroup = this.hass.util.expandGroup;
var sorted = window.HAWS.splitByGroups(states);
byDomain.keySeq().sortBy(function (domain) { return getPriority(domain); })
.forEach(function (domain) {
var priority;
var badgesColl = {};
var beforeGroupColl = {};
var afterGroupedColl = {};
if (domain === 'a') {
cards.demo = true;
return;
}
Object.keys(sorted.ungrouped).forEach(function (key) {
var state = sorted.ungrouped[key];
var domain = computeDomain(state);
priority = getPriority(domain);
if (priority >= 0 && priority < 10) {
cards.badges.push.apply(
cards.badges, filterGrouped(byDomain.get(domain)).sortBy(
entitySortBy).toArray());
} else if (domain === 'group') {
byDomain.get(domain).sortBy(entitySortBy)
.forEach(function (groupState) {
var entities = expandGroup(groupState, states);
entities.forEach(function (entity) { hasGroup[entity.entityId] = true; });
addEntitiesCard(groupState.entityId, entities.toArray(), groupState);
}
);
} else {
addEntitiesCard(
domain, filterGrouped(byDomain.get(domain)).sortBy(entitySortBy).toArray());
}
if (domain === 'a') {
cards.demo = true;
return;
}
);
var priority = getPriority(domain);
var coll;
if (priority < 0) {
coll = beforeGroupColl;
} else if (priority < 10) {
coll = badgesColl;
} else {
coll = afterGroupedColl;
}
if (!(domain in coll)) {
coll[domain] = {
domain: domain,
priority: priority,
states: [],
};
}
coll[domain].states.push(state);
});
iterateDomainSorted(badgesColl, function (domain) {
cards.badges.push.apply(cards.badges, domain.states);
});
iterateDomainSorted(beforeGroupColl, function (domain) {
addEntitiesCard(domain.domain, domain.states);
});
sorted.groups.forEach(function (groupState) {
var entities = window.HAWS.getGroupEntities(states, groupState);
addEntitiesCard(
groupState.entity_id,
Object.keys(entities).map(function (key) {
return entities[key];
}),
groupState
);
});
iterateDomainSorted(afterGroupedColl, function (domain) {
addEntitiesCard(domain.domain, domain.states);
});
// Remove empty columns
cards.columns = cards.columns.filter(function (val) {

View File

@ -36,8 +36,9 @@ Polymer({
return !narrow && showMenu ? 'invisible' : '';
},
toggleMenu: function () {
this.fire('open-menu');
toggleMenu: function (ev) {
ev.stopPropagation();
this.fire('hass-open-menu');
},
});
</script>

View File

@ -0,0 +1,134 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/paper-toggle-button/paper-toggle-button.html">
<dom-module id='ha-push-notifications-toggle'>
<template>
<paper-toggle-button
hidden$='[[!pushSupported]]'
disabled='[[loading]]'
on-change='handlePushChange'
checked='[[pushActive]]'
></paper-toggle-button>
</template>
</dom-module>
<script>
Polymer({
is: 'ha-push-notifications-toggle',
properties: {
hass: { type: Object, value: null },
pushSupported: {
type: Boolean,
readOnly: true,
notify: true,
value: (
'PushManager' in window &&
(document.location.protocol === 'https:' ||
document.location.hostname === 'localhost' ||
document.location.hostname === '127.0.0.1')
)
},
pushActive: {
type: Boolean,
value: 'Notification' in window && Notification.permission === 'granted'
},
loading: {
type: Boolean,
value: true,
}
},
attached: function () {
if (!this.pushSupported) return;
var el = this;
navigator.serviceWorker.ready.then(
function (reg) {
reg.pushManager.getSubscription().then(function (subscription) {
el.loading = false;
el.pushActive = !!subscription;
});
},
function () {
// no service worker.
el._setPushSupported(false);
});
},
handlePushChange: function (ev) {
if (ev.target.checked) {
this.subscribePushNotifications();
} else {
this.unsubscribePushNotifications();
}
},
subscribePushNotifications: function () {
var el = this;
navigator.serviceWorker.ready
.then(function (reg) {
return reg.pushManager.subscribe({ userVisibleOnly: true });
})
.then(
function (sub) {
var browserName;
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
browserName = 'firefox';
} else {
browserName = 'chrome';
}
return el.hass.callApi('POST', 'notify.html5', {
subscription: sub,
browser: browserName
}).then(function () {
el.pushActive = true;
});
},
function (err) {
var message;
if (err.message && err.message.indexOf('gcm_sender_id') !== -1) {
message = 'Please setup the notify.html5 platform.';
} else {
message = 'Notification registration failed.';
}
/* eslint-disable no-console */
console.error(err);
/* eslint-enable no-console */
el.fire('hass-notification', { message: message });
el.pushActive = false;
}
);
},
unsubscribePushNotifications: function () {
var el = this;
navigator.serviceWorker.ready
.then(function (reg) {
return reg.pushManager.getSubscription();
})
.then(function (sub) {
if (!sub) return Promise.resolve();
return el.hass
.callApi('DELETE', 'notify.html5', { subscription: sub })
.then(function () {
sub.unsubscribe();
});
})
.then(function () {
el.pushActive = false;
})
.catch(function (err) {
/* eslint-disable no-console */
console.error('Error in unsub push', err);
/* eslint-enable no-console */
el.fire('hass-notification', {
message: 'Failed unsubscribing for push notifications.'
});
});
}
});
</script>

View File

@ -3,13 +3,12 @@
<link rel='import' href='../../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../../bower_components/paper-menu/paper-menu.html'>
<link rel='import' href='../../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../../bower_components/paper-toggle-button/paper-toggle-button.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="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel='import' href='../util/hass-behavior.html'>
<link rel='import' href='./ha-push-notifications-toggle.html'>
<dom-module id='ha-sidebar'>
<template>
@ -98,13 +97,13 @@
<paper-icon-button icon='mdi:chevron-left' hidden$='[[narrow]]' on-tap='toggleMenu'></paper-icon-button>
</app-toolbar>
<paper-menu attr-for-selected='data-panel' selected='[[selected]]' on-iron-select='menuSelect'>
<paper-menu attr-for-selected='data-panel' selected='[[hass.currentPanel]]' on-iron-select='menuSelect'>
<paper-icon-item on-tap='menuClicked' data-panel='states'>
<iron-icon item-icon icon='mdi:apps'></iron-icon>
<span class='item-text'>States</span>
</paper-icon-item>
<template is='dom-repeat' items='[[computePanels(panels)]]'>
<template is='dom-repeat' items='[[panels]]'>
<paper-icon-item on-tap='menuClicked' data-panel$='[[item.url_path]]'>
<iron-icon item-icon icon='[[item.icon]]'></iron-icon>
<span class='item-text'>[[item.title]]</span>
@ -118,15 +117,15 @@
</paper-menu>
<div>
<template is='dom-if' if='[[supportPush]]'>
<template is='dom-if' if='[[pushSupported]]'>
<div class='divider'></div>
<paper-item class='horizontal layout justified'>
<div class='setting'>Push Notifications</div>
<paper-toggle-button
on-change='handlePushChange'
checked='{{pushToggleChecked}}'
></paper-toggle-button>
<ha-push-notifications-toggle
hass='[[hass]]'
push-supported='{{pushSupported}}'
></ha-push-notifications-toggle>
</paper-item>
</template>
@ -164,8 +163,6 @@
Polymer({
is: 'ha-sidebar',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -183,36 +180,14 @@ Polymer({
type: Boolean,
},
selected: {
type: String,
bindNuclear: function (hass) {
return hass.navigationGetters.activePanelName;
},
},
panels: {
type: Array,
bindNuclear: function (hass) {
return [
hass.navigationGetters.panels,
function (res) { return res.toJS(); },
];
},
computed: 'computePanels(hass)',
},
supportPush: {
pushSupported: {
type: Boolean,
value: false,
bindNuclear: function (hass) {
return hass.pushNotificationGetters.isSupported;
},
},
pushToggleChecked: {
type: Boolean,
bindNuclear: function (hass) {
return hass.pushNotificationGetters.isActive;
},
value: true,
},
},
@ -220,7 +195,8 @@ Polymer({
this._boundUpdateStyles = this.updateStyles.bind(this);
},
computePanels: function (panels) {
computePanels: function (hass) {
var panels = hass.config.panels;
var sortValue = {
map: 1,
logbook: 2,
@ -280,36 +256,22 @@ Polymer({
},
toggleMenu: function () {
this.fire('close-menu');
this.fire('hass-close-menu');
},
selectPanel: function (newChoice) {
if (newChoice === this.selected) {
if (newChoice === this.hass.currentPanel) {
return;
} else if (newChoice === 'logout') {
this.handleLogOut();
return;
}
this.hass.navigationActions.navigate.apply(null, newChoice.split('/'));
this.fire('hass-navigate', { panel: newChoice });
this.debounce('updateStyles', this._boundUpdateStyles, 1);
},
handlePushChange: function (ev) {
if (ev.target.checked) {
this.hass.pushNotificationActions.subscribePushNotifications()
.then(function (success) {
this.pushToggleChecked = success;
}.bind(this));
} else {
this.hass.pushNotificationActions.unsubscribePushNotifications()
.then(function (success) {
this.pushToggleChecked = !success;
}.bind(this));
}
},
handleLogOut: function () {
this.hass.authActions.logOut();
this.fire('hass-logout');
},
});
</script>

View File

@ -0,0 +1,39 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
<dom-module id='ha-start-voice-button'>
<template>
<paper-icon-button
icon="mdi:microphone" hidden$='[[!canListen]]'
on-tap="handleListenClick"
></paper-icon-button>
</template>
</dom-module>
<script>
Polymer({
is: 'ha-start-voice-button',
properties: {
hass: {
type: Object,
value: null,
},
canListen: {
type: Boolean,
computed: 'computeCanListen(hass)',
},
},
computeCanListen: function (hass) {
return ('webkitSpeechRecognition' in window &&
window.hassUtil.isComponentLoaded(hass, 'conversation'));
},
handleListenClick: function () {
this.fire('hass-start-voice');
},
});
</script>

View File

@ -112,7 +112,7 @@
}
startTime = new Date(Math.min.apply(null, deviceStates.map(function (states) {
return states[0].lastChangedAsDate;
return new Date(states[0].last_changed);
})));
endTime = new Date(startTime);
@ -123,8 +123,8 @@
dataTables = deviceStates.map(function (states) {
var last = states[states.length - 1];
var domain = last.domain;
var name = last.entityDisplay;
var domain = window.hassUtil.computeDomain(last);
var name = window.hassUtil.computeStateName(last);
var data = [];
var dataTable = new window.google.visualization.DataTable();
// array containing [time, value1, value2, etc]
@ -168,7 +168,9 @@
var curTemp = saveParseFloat(state.attributes.current_temperature);
var targetHigh = saveParseFloat(state.attributes.target_temp_high);
var targetLow = saveParseFloat(state.attributes.target_temp_low);
pushData([state.lastUpdatedAsDate, curTemp, targetHigh, targetLow], noInterpolations);
pushData(
[new Date(state.last_updated), curTemp, targetHigh, targetLow],
noInterpolations);
};
} else {
dataTable.addColumn('number', name + ' target temperature');
@ -178,7 +180,7 @@
processState = function (state) {
var curTemp = saveParseFloat(state.attributes.current_temperature);
var target = saveParseFloat(state.attributes.temperature);
pushData([state.lastUpdatedAsDate, curTemp, target], noInterpolations);
pushData([new Date(state.last_updated), curTemp, target], noInterpolations);
};
}
@ -203,7 +205,9 @@
var curTemp = saveParseFloat(state.attributes.current_temperature);
var targetHigh = saveParseFloat(state.attributes.target_temp_high);
var targetLow = saveParseFloat(state.attributes.target_temp_low);
pushData([state.lastUpdatedAsDate, curTemp, targetHigh, targetLow], noInterpolations);
pushData(
[new Date(state.last_updated), curTemp, targetHigh, targetLow],
noInterpolations);
};
} else {
dataTable.addColumn('number', name + ' target temperature');
@ -213,7 +217,7 @@
processState = function (state) {
var curTemp = saveParseFloat(state.attributes.current_temperature);
var target = saveParseFloat(state.attributes.temperature);
pushData([state.lastUpdatedAsDate, curTemp, target], noInterpolations);
pushData([new Date(state.last_updated), curTemp, target], noInterpolations);
};
}
@ -226,7 +230,7 @@
states.forEach(function (state) {
var value = saveParseFloat(state.state);
pushData([state.lastChangedAsDate, value], noInterpolations);
pushData([new Date(state.last_changed), value], noInterpolations);
});
}

View File

@ -73,7 +73,7 @@ Polymer({
startTime = new Date(
stateHistory.reduce(
function (minTime, stateInfo) {
return Math.min(minTime, stateInfo[0].lastChangedAsDate);
return Math.min(minTime, new Date(stateInfo[0].last_changed));
}, new Date()));
// end time is Math.min(curTime, start time + 1 day)
@ -93,11 +93,11 @@ Polymer({
if (stateInfo.length === 0) return;
entityDisplay = stateInfo[0].entityDisplay;
entityDisplay = window.hassUtil.computeStateName(stateInfo[0]);
stateInfo.forEach(function (state) {
if (prevState !== null && state.state !== prevState) {
newLastChanged = state.lastChangedAsDate;
newLastChanged = new Date(state.last_changed);
addRow(entityDisplay, prevState, prevLastChanged, newLastChanged);
@ -105,7 +105,7 @@ Polymer({
prevLastChanged = newLastChanged;
} else if (prevState === null) {
prevState = state.state;
prevLastChanged = state.lastChangedAsDate;
prevLastChanged = new Date(state.last_changed);
}
});

View File

@ -24,30 +24,31 @@
}
</style>
<google-legacy-loader on-api-load="googleApiLoaded"></google-legacy-loader>
<google-legacy-loader on-api-load="_googleApiLoaded"></google-legacy-loader>
<div hidden$="[[!isLoading]]" class='loading-container'>
<paper-spinner active alt='Updating history data'></paper-spinner>
</div>
<template is='dom-if' if='[[_isLoading]]'>
<div class='loading-container'>
<paper-spinner active alt='Updating history data'></paper-spinner>
</div>
</template>
<div class$='[[computeContentClasses(isLoading)]]'>
<template is='dom-if' if='[[computeIsEmpty(stateHistory)]]'>
<template is='dom-if' if='[[!_isLoading]]'>
<template is='dom-if' if='[[_computeIsEmpty(historyData)]]'>
No state history found.
</template>
<state-history-chart-timeline
data='[[groupedStateHistory.timeline]]'
is-single-device='[[isSingleDevice]]'>
data='[[historyData.timeline]]'>
</state-history-chart-timeline>
<template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<template is='dom-repeat' items='[[historyData.line]]'>
<state-history-chart-line
unit='[[item.unit]]'
data='[[item.data]]'
is-single-device='[[isSingleDevice]]'>
is-single-device='[[_computeIsSingleLineChart(historyData)]]'>
</state-history-chart-line>
</template>
</div>
</template>
</template>
</dom-module>
@ -56,101 +57,48 @@ Polymer({
is: 'state-history-charts',
properties: {
stateHistory: {
historyData: {
type: Object,
value: null,
},
isLoadingData: {
type: Boolean,
value: false,
value: true,
},
apiLoaded: {
_apiLoaded: {
type: Boolean,
value: false,
},
isLoading: {
_isLoading: {
type: Boolean,
computed: 'computeIsLoading(isLoadingData, apiLoaded)',
},
groupedStateHistory: {
type: Object,
computed: 'computeGroupedStateHistory(isLoading, stateHistory)',
},
isSingleDevice: {
type: Boolean,
computed: 'computeIsSingleDevice(stateHistory)',
computed: '_computeIsLoading(isLoadingData, _apiLoaded)',
},
},
computeIsSingleDevice: function (stateHistory) {
return stateHistory && stateHistory.size === 1;
_computeIsSingleLineChart: function (historyData) {
return historyData && historyData.line.length === 1;
},
computeGroupedStateHistory: function (isLoading, stateHistory) {
var lineChartDevices = {};
var timelineDevices = [];
var unitStates;
if (isLoading || !stateHistory) {
return { line: [], timeline: [] };
}
stateHistory.forEach(function (stateInfo) {
var stateWithUnit;
var unit;
if (!stateInfo || stateInfo.size === 0) {
return;
}
stateWithUnit = stateInfo.find(
function (state) { return 'unit_of_measurement' in state.attributes; });
unit = stateWithUnit ?
stateWithUnit.attributes.unit_of_measurement : false;
if (!unit) {
timelineDevices.push(stateInfo.toArray());
} else if (unit in lineChartDevices) {
lineChartDevices[unit].push(stateInfo.toArray());
} else {
lineChartDevices[unit] = [stateInfo.toArray()];
}
});
timelineDevices = timelineDevices.length > 0 && timelineDevices;
unitStates = Object.keys(lineChartDevices).map(
function (unit) {
return { unit: unit, data: lineChartDevices[unit] };
});
return { line: unitStates, timeline: timelineDevices };
},
googleApiLoaded: function () {
_googleApiLoaded: function () {
window.google.load('visualization', '1', {
packages: ['timeline', 'corechart'],
callback: function () {
this.apiLoaded = true;
this._apiLoaded = true;
}.bind(this),
});
},
computeContentClasses: function (isLoading) {
return isLoading ? 'loading' : '';
_computeIsLoading: function (_isLoadingData, _apiLoaded) {
return _isLoadingData || !_apiLoaded;
},
computeIsLoading: function (isLoadingData, apiLoaded) {
return isLoadingData || !apiLoaded;
},
computeIsEmpty: function (stateHistory) {
return stateHistory && stateHistory.size === 0;
_computeIsEmpty: function (historyData) {
return (historyData &&
historyData.timeline.length === 0 &&
historyData.line.length === 0);
},
});
</script>

View File

@ -0,0 +1,162 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<script>
(function () {
var RECENT_THRESHOLD = 60000; // 1 minute
var DATE_CACHE = {};
var RECENT_CACHE = {};
function computeHistory(stateHistory) {
var lineChartDevices = {};
var timelineDevices = [];
var unitStates;
if (!stateHistory) {
return { line: [], timeline: [] };
}
stateHistory.forEach(function (stateInfo) {
var stateWithUnit;
var unit;
if (stateInfo.size === 0) {
return;
}
stateWithUnit = stateInfo.find(
function (state) { return 'unit_of_measurement' in state.attributes; });
unit = stateWithUnit ?
stateWithUnit.attributes.unit_of_measurement : false;
if (!unit) {
timelineDevices.push(stateInfo);
} else if (unit in lineChartDevices) {
lineChartDevices[unit].push(stateInfo);
} else {
lineChartDevices[unit] = [stateInfo];
}
});
timelineDevices = timelineDevices.length > 0 && timelineDevices;
unitStates = Object.keys(lineChartDevices).map(
function (unit) {
return { unit: unit, data: lineChartDevices[unit] };
});
return { line: unitStates, timeline: timelineDevices };
}
Polymer({
is: 'ha-state-history-data',
properties: {
hass: {
type: Object,
observer: 'hassChanged',
},
filterType: {
type: String,
},
filterValue: {
type: String,
},
isLoading: {
type: Boolean,
value: true,
readOnly: true,
notify: true,
},
data: {
type: Object,
value: null,
readOnly: true,
notify: true,
},
},
observers: [
'filterChanged(filterType, filterValue)',
],
hassChanged: function (newHass, oldHass) {
if (!oldHass && this.filterType && this.filterValue) {
this.filterChanged(this.filterType, this.filterValue);
}
},
filterChanged: function (filterType, filterValue) {
if (!this.hass) return;
var data;
if (filterType === 'date') {
data = this.getDate(filterValue);
} else if (filterType === 'recent-entity') {
data = this.getRecent(filterValue);
} else {
return;
}
this._setIsLoading(true);
data.then(function (stateHistory) {
this._setData(stateHistory);
this._setIsLoading(false);
}.bind(this));
},
getRecent: function (entityId) {
var cache = RECENT_CACHE[entityId];
if (cache && Date.now() - cache.created < RECENT_THRESHOLD) {
return cache.data;
}
var url = 'history/period';
if (entityId) {
url += '?filter_entity_id=' + entityId;
}
var prom = this.hass.callApi('GET', url).then(
function (stateHistory) {
return computeHistory(stateHistory);
},
function () {
RECENT_CACHE[entityId] = false;
return null;
}
);
RECENT_CACHE[entityId] = {
created: Date.now(),
data: prom,
};
return prom;
},
getDate: function (date) {
if (!DATE_CACHE[date]) {
DATE_CACHE[date] = this.hass.callApi('GET', 'history/period/' + date).then(
function (stateHistory) {
return computeHistory(stateHistory);
},
function () {
DATE_CACHE[date] = false;
return null;
}
);
}
return DATE_CACHE[date];
},
});
}());
</script>

View File

@ -5,8 +5,6 @@
<link rel="import" href="../../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
<link rel='import' href='../util/hass-behavior.html'>
<dom-module id="ha-voice-command-dialog">
<template>
<style>
@ -29,6 +27,10 @@
margin-right: 24px;
}
.error {
color: red;
}
.interimTranscript {
color: darkgrey;
}
@ -54,11 +56,14 @@
<iron-icon icon="mdi:text-to-speech" hidden$="[[isTransmitting]]"></iron-icon>
<paper-spinner active$="[[isTransmitting]]" hidden$="[[!isTransmitting]]"></paper-spinner>
</div>
<div class='text'>
<span>{{finalTranscript}}</span>
<span class='interimTranscript'>[[interimTranscript]]</span>
<div class='text' hidden$='[[hasError]]'>
<span>{{results.final}}</span>
<span class='interimTranscript'>[[results.interim]]</span>
</div>
<div class='text red' hidden$='[[!hasError]]'>
An error occurred. Unable to fulfill request.
</div>
</div>
</paper-dialog>
</template>
@ -69,8 +74,6 @@
Polymer({
is: 'ha-voice-command-dialog',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -82,32 +85,23 @@ Polymer({
observer: 'dialogOpenChanged',
},
finalTranscript: {
type: String,
bindNuclear: function (hass) {
return hass.voiceGetters.finalTranscript;
},
},
interimTranscript: {
type: String,
bindNuclear: function (hass) {
return hass.voiceGetters.extraInterimTranscript;
},
results: {
type: Object,
},
isTransmitting: {
type: Boolean,
bindNuclear: function (hass) {
return hass.voiceGetters.isTransmitting;
},
value: false,
},
isListening: {
type: Boolean,
bindNuclear: function (hass) {
return hass.voiceGetters.isListening;
},
value: false,
},
hasError: {
type: Boolean,
value: false,
},
showListenInterface: {
@ -117,13 +111,72 @@ Polymer({
},
},
initRecognition: function () {
/* eslint-disable new-cap */
this.recognition = new webkitSpeechRecognition();
/* eslint-enable new-cap */
this.recognition.onstart = function () {
this.isListening = true;
this.isTransmitting = false;
this.hasError = false;
this.results = {
final: '',
interim: '',
};
}.bind(this);
this.recognition.onerror = function () {
this.recognition.abort();
this.hasError = true;
}.bind(this);
this.recognition.onend = function () {
this.isListening = false;
this.isTransmitting = true;
var text = this.results.final || this.results.interim;
var listeningDone = function () {
this.isTransmitting = false;
}.bind(this);
this.hass.callService('conversation', 'process', { text: text })
.then(listeningDone, listeningDone);
}.bind(this);
this.recognition.onresult = function (event) {
var oldResults = this.results;
var finalTranscript = '';
var interimTranscript = '';
for (var ind = event.resultIndex; ind < event.results.length; ind++) {
if (event.results[ind].isFinal) {
finalTranscript += event.results[ind][0].transcript;
} else {
interimTranscript += event.results[ind][0].transcript;
}
}
this.results = {
interim: interimTranscript,
final: oldResults.final + finalTranscript,
};
}.bind(this);
},
startListening: function () {
if (!this.recognition) {
this.initRecognition();
}
this.recognition.start();
},
computeShowListenInterface: function (isListening, isTransmitting) {
return isListening || isTransmitting;
},
dialogOpenChanged: function (newVal) {
if (!newVal && this.isListening) {
this.hass.voiceActions.stop();
this.recognition.abort();
}
},
@ -135,4 +188,4 @@ Polymer({
}
},
});
</script>
</script>

View File

@ -8,8 +8,7 @@
<link rel="import" href="../state-summary/state-card-content.html">
<link rel="import" href="../components/state-history-charts.html">
<link rel="import" href="../more-infos/more-info-content.html">
<link rel='import' href='../util/hass-behavior.html'>
<link rel="import" href="../data/ha-state-history-data.html">
<dom-module id="more-info-dialog">
<template>
@ -51,15 +50,24 @@
</style>
<!-- entry-animation='slide-up-animation' exit-animation='slide-down-animation' -->
<paper-dialog id="dialog" with-backdrop opened='{{dialogOpen}}' data-domain$='[[stateObj.domain]]'>
<paper-dialog id="dialog" with-backdrop opened='{{dialogOpen}}' data-domain$='[[computeDomain(stateObj)]]'>
<h2>
<state-card-content
state-obj="[[stateObj]]"
hass='[[hass]]' in-dialog></state-card-content>
</h2>
<template is='dom-if' if="[[showHistoryComponent]]">
<state-history-charts state-history="[[stateHistory]]"
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
<div>
<ha-state-history-data
hass='[[hass]]'
filter-type='[[_filterType]]'
filter-value='[[stateObj.entity_id]]'
data='{{stateHistory}}'
is-loading='{{stateHistoryLoading}}'
></ha-state-history-data>
<state-history-charts history-data="[[stateHistory]]"
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
</div>
</template>
<paper-dialog-scrollable id='scrollable'>
<more-info-content
@ -73,8 +81,6 @@
Polymer({
is: 'more-info-dialog',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -82,48 +88,26 @@ Polymer({
stateObj: {
type: Object,
bindNuclear: function (hass) {
return hass.moreInfoGetters.currentEntity;
},
computed: 'computeStateObj(hass)',
observer: 'stateObjChanged',
},
stateHistory: {
type: Object,
bindNuclear: function (hass) {
return [
hass.moreInfoGetters.currentEntityHistory,
function (history) { return history ? [history] : false; },
];
},
},
stateHistoryLoading: {
type: Boolean,
},
isLoadingHistoryData: {
type: Boolean,
computed: 'computeIsLoadingHistoryData(delayedDialogOpen, isLoadingEntityHistoryData)',
},
isLoadingEntityHistoryData: {
type: Boolean,
bindNuclear: function (hass) {
return hass.entityHistoryGetters.isLoadingEntityHistory;
},
computed: 'computeIsLoadingHistoryData(delayedDialogOpen, stateHistoryLoading)',
},
hasHistoryComponent: {
type: Boolean,
bindNuclear: function (hass) {
return hass.configGetters.isComponentLoaded('history');
},
observer: 'fetchHistoryData',
},
shouldFetchHistory: {
type: Boolean,
bindNuclear: function (hass) {
return hass.moreInfoGetters.isCurrentEntityHistoryStale;
},
observer: 'fetchHistoryData',
computed: 'computeHasHistoryComponent(hass)',
},
showHistoryComponent: {
@ -142,32 +126,43 @@ Polymer({
type: Boolean,
value: false,
},
_filterType: {
type: String,
value: 'recent-entity',
},
},
ready: function () {
this.$.scrollable.dialogElement = this.$.dialog;
},
computeDomain: function (stateObj) {
return stateObj ? window.hassUtil.computeDomain(stateObj) : '';
},
computeStateObj: function (hass) {
return hass.states[hass.moreInfoEntityId] || null;
},
/**
* We depend on a delayed dialogOpen value to tell the chart component
* that the data is there. Otherwise the chart component will render
* before the dialog is attached to the screen and is unable to determine
* graph size resulting in scroll bars.
*/
computeIsLoadingHistoryData: function (delayedDialogOpen, isLoadingEntityHistoryData) {
return !delayedDialogOpen || isLoadingEntityHistoryData;
computeIsLoadingHistoryData: function (delayedDialogOpen, stateHistoryLoading) {
return !delayedDialogOpen || stateHistoryLoading;
},
computeHasHistoryComponent: function (hass) {
return window.hassUtil.isComponentLoaded(hass, 'history');
},
computeShowHistoryComponent: function (hasHistoryComponent, stateObj) {
return this.hasHistoryComponent && stateObj &&
window.hassUtil.DOMAINS_WITH_NO_HISTORY.indexOf(stateObj.domain) === -1;
},
fetchHistoryData: function () {
if (this.stateObj && this.hasHistoryComponent &&
this.shouldFetchHistory) {
this.hass.entityHistoryActions.fetchRecent(this.stateObj.entityId);
}
window.hassUtil.DOMAINS_WITH_NO_HISTORY.indexOf(
window.hassUtil.computeDomain(stateObj)) === -1;
},
stateObjChanged: function (newVal) {
@ -177,8 +172,6 @@ Polymer({
}
this.async(function () {
// Firing action while other action is happening confuses nuclear
this.fetchHistoryData();
// allow dialog to render content before showing it so it is
// positioned correctly.
this.dialogOpen = true;
@ -189,7 +182,7 @@ Polymer({
if (newVal) {
this.async(function () { this.delayedDialogOpen = true; }.bind(this), 10);
} else if (!newVal && this.stateObj) {
this.async(function () { this.hass.moreInfoActions.deselectEntity(); }.bind(this), 10);
this.fire('hass-more-info', { entityId: null });
this.delayedDialogOpen = false;
}
},

View File

@ -6,28 +6,47 @@
<link rel='import' href='../bower_components/iron-flex-layout/iron-flex-layout-classes.html'>
<link rel='import' href='./util/hass-util.html'>
<link rel='import' href='./util/hass-behavior.html'>
<link rel='import' href='./util/ha-pref-storage.html'>
<link rel='import' href='./util/hass-call-api.html'>
<link rel='import' href='./layouts/login-form.html'>
<link rel='import' href='./layouts/home-assistant-main.html'>
<link rel='import' href='./resources/ha-style.html'>
<link rel="import" href="./resources/panel-imports.html">
<link rel='import' href='./managers/notification-manager.html'>
<dom-module id='home-assistant'>
<template>
<template is='dom-if' if='[[loaded]]'>
<home-assistant-main hass='[[hass]]'></home-assistant-main>
<ha-pref-storage hass='[[hass]]' id='storage'></ha-pref-storage>
<notification-manager id='notifications' hass='[[hass]]'></notification-manager>
<template is='dom-if' if='[[showMain]]' restamp>
<home-assistant-main
on-hass-more-info='handleMoreInfo'
on-hass-navigate='handleNavigate'
on-hass-dock-sidebar='handleDockSidebar'
on-hass-notification='handleNotification'
on-hass-logout='handleLogout'
hass='[[hass]]'
></home-assistant-main>
</template>
<template is='dom-if' if='[[!loaded]]'>
<template is='dom-if' if='[[!showMain]]'>
<login-form
hass='[[hass]]'
force-show-loading='[[computeForceShowLoading(dataLoaded, iconsLoaded)]]'>
connection-promise='{{connectionPromise}}'
show-loading='[[computeShowLoading(connectionPromise, iconsLoaded)]]'>
</login-form>
</template>
</template>
</dom-module>
<script>
window.removeInitMsg = function () {
var initMsg = document.getElementById('ha-init-skeleton');
if (initMsg) {
initMsg.parentElement.removeChild(initMsg);
}
};
Polymer({
is: 'home-assistant',
@ -35,36 +54,40 @@ Polymer({
icons: null,
},
behaviors: [window.hassBehavior],
properties: {
connectionPromise: {
type: Object,
value: window.hassConnection || null,
observer: 'handleConnectionPromise',
},
connection: {
type: Object,
value: null,
observer: 'connectionChanged',
},
hass: {
type: Object,
value: window.hass,
value: null,
},
icons: {
type: String,
},
dataLoaded: {
type: Boolean,
bindNuclear: function (hass) { return hass.syncGetters.isDataLoaded; },
},
iconsLoaded: {
type: Boolean,
value: false,
},
loaded: {
showMain: {
type: Boolean,
computed: 'computeLoaded(dataLoaded, iconsLoaded)',
computed: 'computeShowMain(hass, iconsLoaded)',
},
},
computeLoaded: function (dataLoaded, iconsLoaded) {
return dataLoaded && iconsLoaded;
computeShowMain: function (hass, iconsLoaded) {
return hass && hass.states && hass.config && iconsLoaded;
},
computeForceShowLoading: function (dataLoaded, iconsLoaded) {
return dataLoaded && !iconsLoaded;
computeShowLoading: function (connectionPromise) {
return connectionPromise != null;
},
loadIcons: function () {
@ -81,8 +104,125 @@ Polymer({
});
},
connectionChanged: function (conn) {
if (!conn) {
this.hass = null;
return;
}
var notifications = this.$.notifications;
this.hass = Object.assign({
connection: conn,
connected: true,
states: null,
config: null,
dockedSidebar: false,
currentPanel: 'states',
currentView: null,
moreInfoEntityId: null,
callService: function (domain, service, serviceData) {
return conn.callService(domain, service, serviceData || {})
.then(function () {
var message;
if (service === 'turn_on' && serviceData.entity_id) {
message = 'Turned on ' + serviceData.entity_id + '.';
} else if (service === 'turn_off' && serviceData.entity_id) {
message = 'Turned off ' + serviceData.entity_id + '.';
} else {
message = 'Service ' + domain + '/' + service + ' called.';
}
notifications.showNotification(message);
},
function () {
notifications.showNotification(
'Failed to call service ' + domain + '/' + service);
return Promise.reject();
});
},
callApi: function (method, path, parameters) {
var host = window.location.protocol + '//' + window.location.host;
return window.hassCallApi(host, {}, method, path, parameters);
},
}, this.$.storage.getStoredState());
conn.addEventListener('ready', function () {
this.hass = Object.assign({}, this.hass, { connected: true });
}.bind(this));
conn.addEventListener('disconnected', function () {
this.hass = Object.assign({}, this.hass, { connected: false });
}.bind(this));
window.HAWS.subscribeEntities(conn, function (states) {
this.hass = Object.assign({}, this.hass, { states: states });
}.bind(this));
window.HAWS.subscribeConfig(conn, function (config) {
this.hass = Object.assign({}, this.hass, { config: config });
}.bind(this));
},
handleConnectionPromise: function (prom) {
if (!prom) return;
var el = this;
prom.then(function (conn) {
el.connection = conn;
}, function () {
el.connectionPromise = null;
});
},
handleMoreInfo: function (ev) {
ev.stopPropagation();
this.hass = Object.assign(
{}, this.hass,
{ moreInfoEntityId: ev.detail.entityId });
},
handleNavigate: function (ev) {
ev.stopPropagation();
var hass = Object.assign({}, this.hass);
if ('panel' in ev.detail) {
hass.currentPanel = ev.detail.panel;
}
if ('view' in ev.detail) {
hass.currentView = ev.detail.view;
}
this.hass = hass;
},
handleDockSidebar: function (ev) {
ev.stopPropagation();
this.hass = Object.assign(
{}, this.hass,
{ dockedSidebar: ev.detail.dock });
this.$.storage.storeState();
},
handleNotification: function (ev) {
this.$.notifications.showNotification(ev.detail.message);
},
handleLogout: function () {
this.connection.close();
delete localStorage.authToken;
this.connection = null;
this.connectionPromise = null;
this.hass = null;
},
ready: function () {
this.loadIcons();
if (this.connectionPromise !== null) {
this.handleConnectionPromise(this.connectionPromise);
}
},
});
</script>

View File

@ -5,22 +5,25 @@
<link rel='import' href='../layouts/partial-cards.html'>
<link rel='import' href='../layouts/partial-panel-resolver.html'>
<link rel='import' href='../managers/notification-manager.html'>
<link rel="import" href="../dialogs/more-info-dialog.html">
<link rel="import" href="../dialogs/ha-voice-command-dialog.html">
<link rel='import' href='../util/ha-url-sync.html'>
<link rel='import' href='../components/ha-sidebar.html'>
<dom-module id='home-assistant-main'>
<template>
<notification-manager hass='[[hass]]'></notification-manager>
<more-info-dialog hass='[[hass]]'></more-info-dialog>
<ha-voice-command-dialog hass='[[hass]]'></ha-voice-command-dialog>
<ha-url-sync hass='[[hass]]'></ha-url-sync>
<ha-voice-command-dialog
hass='[[hass]]'
id='voiceDialog'
></ha-voice-command-dialog>
<iron-media-query query="(max-width: 870px)" query-matches="{{narrow}}">
</iron-media-query>
<paper-drawer-panel id='drawer'
force-narrow='[[computeForceNarrow(narrow, showSidebar)]]'
force-narrow='[[computeForceNarrow(narrow, dockedSidebar)]]'
responsive-width='0' disable-swipe='[[isSelectedMap]]'
disable-edge-swipe='[[isSelectedMap]]'>
<ha-sidebar drawer narrow='[[narrow]]' hass='[[hass]]'></ha-sidebar>
@ -29,21 +32,21 @@
main
attr-for-selected='id'
fallback-selection='panel-resolver'
selected='[[activePanel]]'
selected='[[currentPanel]]'
selected-attribute='panel-visible'
>
<partial-cards
id='states'
narrow='[[narrow]]'
hass='[[hass]]'
show-menu='[[showSidebar]]'
show-menu='[[dockedSidebar]]'
></partial-cards>
<partial-panel-resolver
id='panel-resolver'
narrow='[[narrow]]'
hass='[[hass]]'
show-menu='[[showSidebar]]'
show-menu='[[dockedSidebar]]'
></partial-panel-resolver>
</iron-pages>
@ -56,56 +59,55 @@
Polymer({
is: 'home-assistant-main',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
value: null,
},
narrow: {
type: Boolean,
value: true,
},
activePanel: {
currentPanel: {
type: String,
bindNuclear: function (hass) {
return hass.navigationGetters.activePanelName;
},
observer: 'activePanelChanged',
computed: 'computeCurrentPanel(hass)',
observer: 'currentPanelChanged',
},
showSidebar: {
dockedSidebar: {
type: Boolean,
value: false,
bindNuclear: function (hass) {
return hass.navigationGetters.showSidebar;
},
computed: 'computeDockedSidebar(hass)',
},
},
listeners: {
'open-menu': 'openMenu',
'close-menu': 'closeMenu',
'hass-open-menu': 'handleOpenMenu',
'hass-close-menu': 'handleCloseMenu',
'hass-start-voice': 'handleStartVoice',
},
openMenu: function () {
handleStartVoice: function (ev) {
ev.stopPropagation();
this.$.voiceDialog.startListening();
},
handleOpenMenu: function () {
if (this.narrow) {
this.$.drawer.openDrawer();
} else {
this.hass.navigationActions.showSidebar(true);
this.fire('hass-dock-sidebar', { dock: true });
}
},
closeMenu: function () {
handleCloseMenu: function () {
this.$.drawer.closeDrawer();
if (this.showSidebar) {
this.hass.navigationActions.showSidebar(false);
if (this.dockedSidebar) {
this.fire('hass-dock-sidebar', { dock: false });
}
},
activePanelChanged: function () {
currentPanelChanged: function () {
if (this.narrow) {
this.$.drawer.closeDrawer();
}
@ -113,15 +115,18 @@ Polymer({
attached: function () {
window.removeInitMsg();
this.hass.startUrlSync();
},
computeForceNarrow: function (narrow, showSidebar) {
return narrow || !showSidebar;
computeForceNarrow: function (narrow, dockedSidebar) {
return narrow || !dockedSidebar;
},
detached: function () {
this.hass.stopUrlSync();
computeCurrentPanel: function (hass) {
return hass.currentPanel;
},
computeDockedSidebar: function (hass) {
return hass.dockedSidebar;
},
});
</script>

View File

@ -9,8 +9,6 @@
<link rel="import" href="../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
<link rel='import' href='../util/hass-behavior.html'>
<dom-module id="login-form">
<template>
<style is="custom-style" include="iron-flex iron-positioning"></style>
@ -51,10 +49,18 @@
<a href="#" id="hideKeyboardOnFocus"></a>
<div class='interact'>
<div id='loginform' hidden$="[[showLoading]]">
<paper-input-container id="passwordDecorator" invalid="[[isInvalid]]">
<paper-input-container
id="passwordDecorator"
invalid="[[errorMessage]]"
>
<label>Password</label>
<input is="iron-input" type="password" id="passwordInput" />
<paper-input-error invalid="[[isInvalid]]">[[errorMessage]]</paper-input-error>
<input
is="iron-input"
type="password"
id="passwordInput"
bind-value="{{password}}"
/>
<paper-input-error invalid="[[errorMessage]]">[[errorMessage]]</paper-input-error>
</paper-input-container>
<div class="layout horizontal center">
<paper-checkbox for id='rememberLogin'>Remember</paper-checkbox>
@ -74,31 +80,25 @@
Polymer({
is: 'login-form',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
},
errorMessage: {
type: String,
bindNuclear: function (hass) { return hass.authGetters.attemptErrorMessage; },
connectionPromise: {
type: Object,
notify: true,
observer: 'handleConnectionPromiseChanged',
},
isInvalid: {
type: Boolean,
bindNuclear: function (hass) { return hass.authGetters.isInvalidAttempt; },
errorMessage: {
type: String,
value: '',
},
isValidating: {
type: Boolean,
observer: 'isValidatingChanged',
bindNuclear: function (hass) { return hass.authGetters.isValidating; },
},
loadingResources: {
type: Boolean,
value: false,
},
@ -111,6 +111,11 @@ Polymer({
type: Boolean,
computed: 'computeShowSpinner(forceShowLoading, isValidating)',
},
password: {
type: String,
value: '',
},
},
listeners: {
@ -118,10 +123,6 @@ Polymer({
'loginButton.tap': 'validatePassword',
},
observers: [
'validatingChanged(isValidating, isInvalid)',
],
attached: function () {
window.removeInitMsg();
},
@ -130,15 +131,11 @@ Polymer({
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);
this.async(function () {
this.$.passwordInput.focus();
}.bind(this), 10);
}
},
@ -148,16 +145,46 @@ Polymer({
this.validatePassword();
ev.preventDefault();
// clear error after we start typing again
} else if (this.isInvalid) {
this.isInvalid = false;
} else if (this.errorMessage) {
this.errorMessage = '';
}
},
validatePassword: function () {
var auth = this.password;
this.$.hideKeyboardOnFocus.focus();
this.connectionPromise = window.createHassConnection(auth);
window.validateAuth(this.$.passwordInput.value,
this.$.rememberLogin.checked);
if (this.$.rememberLogin.checked) {
this.connectionPromise.then(function () {
localStorage.authToken = auth;
});
}
},
handleConnectionPromiseChanged: function (newVal) {
if (!newVal) return;
var el = this;
this.isValidating = true;
this.connectionPromise.then(
function () {
el.isValidating = false;
el.password = '';
},
function (errCode) {
el.isValidating = false;
if (errCode === window.HAWS.ERR_CANNOT_CONNECT) {
el.errorMessage = 'Unable to connect';
} else if (errCode === window.HAWS.ERR_INVALID_AUTH) {
el.errorMessage = 'Invalid password';
} else {
el.errorMessage = 'Unknown error: ' + errCode;
}
}
);
}
});
</script>

View File

@ -4,7 +4,6 @@
<link rel="import" href="../../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../../bower_components/iron-pages/iron-pages.html">
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../../bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="../../bower_components/paper-tabs/paper-tab.html">
@ -14,8 +13,8 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../components/ha-menu-button.html">
<link rel="import" href="../components/ha-start-voice-button.html">
<link rel="import" href="../components/ha-cards.html">
<link rel="import" href="../util/hass-behavior.html">
<dom-module id="partial-cards">
<template>
@ -42,9 +41,7 @@
<app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>[[computeTitle(views, locationName)]]</div>
<paper-icon-button
icon="mdi:microphone" hidden$='[[!canListen]]'
on-tap="handleListenClick"></paper-icon-button>
<ha-start-voice-button hass='[[hass]]'></ha-start-voice-button>
</app-toolbar>
<div sticky hidden$='[[!views.length]]'>
@ -60,14 +57,14 @@
>[[locationName]]</paper-tab>
<template is='dom-repeat' items='[[views]]'>
<paper-tab
data-entity$='[[item.entityId]]'
data-entity$='[[item.entity_id]]'
on-tap='scrollToTop'
>
<template is='dom-if' if='[[item.attributes.icon]]'>
<iron-icon icon='[[item.attributes.icon]]'></iron-icon>
</template>
<template is='dom-if' if='[[!item.attributes.icon]]'>
[[item.entityDisplay]]
[[computeStateName(item)]]
</template>
</paper-tab>
</template>
@ -82,8 +79,8 @@
>
<ha-cards
data-view=''
show-introduction='[[computeShowIntroduction(currentView, introductionLoaded, states)]]'
states='[[states]]'
show-introduction='[[computeShowIntroduction(currentView, introductionLoaded, viewStates)]]'
states='[[viewStates]]'
columns='[[_columns]]'
hass='[[hass]]'
panel-visible='[[panelVisible]]'
@ -91,9 +88,9 @@
<template is='dom-repeat' items='[[views]]'>
<ha-cards
data-view$='[[item.entityId]]'
show-introduction='[[computeShowIntroduction(currentView, introductionLoaded, states)]]'
states='[[states]]'
data-view$='[[item.entity_id]]'
show-introduction='[[computeShowIntroduction(currentView, introductionLoaded, viewStates)]]'
states='[[viewStates]]'
columns='[[_columns]]'
hass='[[hass]]'
panel-visible='[[panelVisible]]'
@ -108,13 +105,15 @@
<script>
Polymer({
is: 'partial-cards',
DEFAULT_VIEW_ENTITY_ID: 'group.default_view',
ALWAYS_SHOW_DOMAIN: ['persistent_notification', 'configurator'],
behaviors: [window.hassBehavior],
is: 'partial-cards',
properties: {
hass: {
type: Object,
value: null,
},
narrow: {
@ -124,7 +123,6 @@ Polymer({
showMenu: {
type: Boolean,
value: false,
observer: 'handleWindowChange',
},
@ -138,62 +136,30 @@ Polymer({
value: 1,
},
canListen: {
type: Boolean,
bindNuclear: function (hass) {
return [
hass.voiceGetters.isVoiceSupported,
hass.configGetters.isComponentLoaded('conversation'),
function (isVoiceSupported, componentLoaded) {
return isVoiceSupported && componentLoaded;
},
];
},
},
introductionLoaded: {
type: Boolean,
bindNuclear: function (hass) {
return hass.configGetters.isComponentLoaded('introduction');
},
computed: 'computeIntroductionLoaded(hass)',
},
locationName: {
type: String,
bindNuclear: function (hass) {
return hass.configGetters.locationName;
},
value: '',
computed: 'computeLocationName(hass)',
},
currentView: {
type: String,
bindNuclear: function (hass) {
return [
hass.viewGetters.currentView,
function (view) { return view || ''; },
];
},
computed: 'computeCurrentView(hass)',
},
views: {
type: Array,
bindNuclear: function (hass) {
return [
hass.viewGetters.views,
function (views) {
return views.valueSeq()
.sortBy(function (view) { return view.attributes.order; })
.toArray();
},
];
},
computed: 'computeViews(hass)',
},
states: {
viewStates: {
type: Object,
bindNuclear: function (hass) {
return hass.viewGetters.currentViewEntities;
},
computed: 'computeViewStates(currentView, hass)',
},
},
@ -259,17 +225,11 @@ Polymer({
}).call(this);
},
handleListenClick: function () {
this.hass.voiceActions.listen();
},
handleViewSelected: function (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);
this.fire('hass-navigate', { view: view });
}
},
@ -280,5 +240,72 @@ Polymer({
computeShowIntroduction: function (currentView, introductionLoaded, states) {
return currentView === '' && (introductionLoaded || states.size === 0);
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
},
computeLocationName: function (hass) {
return window.hassUtil.computeLocationName(hass);
},
computeIntroductionLoaded: function (hass) {
return window.hassUtil.isComponentLoaded(hass, 'introduction');
},
computeViews: function (hass) {
return window.HAWS.extractViews(hass.states);
},
/*
Compute the states to show for current view.
Will make sure we always show entities from ALWAYS_SHOW_DOMAINS domains.
*/
computeViewStates: function (currentView, hass) {
var i;
var entityId;
var state;
var states;
var entityIds = Object.keys(hass.states);
// If we base off all entities, only have to filter out hidden
if (!currentView && !(this.DEFAULT_VIEW_ENTITY_ID in hass.states)) {
states = {};
for (i = 0; i < entityIds.length; i++) {
entityId = entityIds[i];
state = hass.states[entityId];
// We can filter out hidden and domain at the same time.
if (!state.attributes.hidden) {
states[entityId] = state;
}
}
return states;
}
if (currentView) {
states = window.HAWS.getViewEntities(hass.states, hass.states[currentView]);
} else {
states = window.HAWS.getViewEntities(
hass.states, hass.states[this.DEFAULT_VIEW_ENTITY_ID]);
}
// Make sure certain domains are always shown.
for (i = 0; i < entityIds.length; i++) {
entityId = entityIds[i];
state = hass.states[entityId];
if (this.ALWAYS_SHOW_DOMAIN.indexOf(window.hassUtil.computeDomain(state)) !== -1) {
states[entityId] = state;
}
}
return states;
},
computeCurrentView: function (hass) {
return hass.currentView || '';
},
});
</script>

View File

@ -5,7 +5,6 @@
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel='import' href='../components/ha-menu-button.html'>
<link rel='import' href='../util/hass-behavior.html'>
<link rel="import" href="../resources/ha-style.html">
<dom-module id='partial-panel-resolver'>
@ -46,8 +45,6 @@
Polymer({
is: 'partial-panel-resolver',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -78,13 +75,15 @@ Polymer({
panel: {
type: Object,
bindNuclear: function (hass) {
return hass.navigationGetters.activePanel;
},
computed: 'computeCurrentPanel(hass)',
observer: 'panelChanged',
},
},
computeCurrentPanel: function (hass) {
return hass.config.panels[hass.currentPanel];
},
panelChanged: function (panel) {
if (!panel) {
if (this.$.panel.lastChild) {
@ -97,15 +96,15 @@ Polymer({
this.errorLoading = false;
this.importHref(
panel.get('url'),
panel.url,
function success() {
window.hassUtil.dynamicContentUpdater(
this.$.panel, 'ha-panel-' + panel.get('component_name'), {
this.$.panel, 'ha-panel-' + panel.component_name, {
hass: this.hass,
narrow: this.narrow,
showMenu: this.showMenu,
panel: panel.toJS(),
panel: panel,
});
this.resolved = true;
}.bind(this),

View File

@ -2,8 +2,6 @@
<link rel="import" href="../../bower_components/paper-toast/paper-toast.html">
<link rel='import' href='../util/hass-behavior.html'>
<dom-module id="notification-manager">
<template>
<style>
@ -14,8 +12,8 @@
<paper-toast
id="toast"
text='{{text}}'
no-cancel-on-outside-click='[[neg]]'
text='[[_text]]'
no-cancel-on-outside-click='[[_cancelOnOutsideClick]]'
></paper-toast>
<paper-toast
id='connToast'
@ -30,8 +28,6 @@
Polymer({
is: 'notification-manager',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -39,21 +35,17 @@ Polymer({
isStreaming: {
type: Boolean,
bindNuclear: function (hass) { return hass.streamGetters.isStreamingEvents; },
computed: 'computeIsStreaming(hass)',
},
// Otherwise we cannot close a modal when a notification is being shown.
neg: {
_cancelOnOutsideClick: {
type: Boolean,
value: false,
},
text: {
_text: {
type: String,
bindNuclear: function (hass) {
return hass.notificationGetters.lastNotificationMessage;
},
observer: 'showNotification',
readOnly: true,
},
toastClass: {
@ -62,6 +54,10 @@ Polymer({
},
},
computeIsStreaming: function (hass) {
return !hass || hass.connected;
},
created: function () {
this.handleWindowChange = this.handleWindowChange.bind(this);
this._mediaq = window.matchMedia('(max-width: 599px)');
@ -81,10 +77,9 @@ Polymer({
this.$.connToast.classList.toggle('fit-bottom', ev.matches);
},
showNotification: function (newText) {
if (newText) {
this.$.toast.show();
}
showNotification: function (message) {
this._set_text(message);
this.$.toast.show();
},
});
</script>

View File

@ -129,8 +129,8 @@ Polymer({
callService: function (service, data) {
var serviceData = data || {};
serviceData.entity_id = this.stateObj.entityId;
this.hass.serviceActions.callService('alarm_control_panel', service, serviceData)
serviceData.entity_id = this.stateObj.entity_id;
this.hass.callService('alarm_control_panel', service, serviceData)
.then(function () {
this.enteredCode = '';
}.bind(this));

View File

@ -42,8 +42,8 @@ Polymer({
},
handleTriggerTapped: function () {
this.hass.serviceActions.callService('automation', 'trigger', {
entity_id: this.stateObj.entityId,
this.hass.callService('automation', 'trigger', {
entity_id: this.stateObj.entity_id,
});
},
});

View File

@ -13,7 +13,7 @@
</style>
<img class='camera-image' src="[[computeCameraImageUrl(hass, stateObj, isVisible)]]"
on-load='imageLoaded' alt='[[stateObj.entityDisplay]]' />
on-load='imageLoaded' alt='[[computeStateName(stateObj)]]' />
</template>
</dom-module>
@ -40,11 +40,15 @@ Polymer({
this.fire('iron-resize');
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
},
computeCameraImageUrl: function (hass, stateObj, isVisible) {
if (hass.demo) {
return '/demo/webcam.jpg';
} else if (stateObj && isVisible) {
return '/api/camera_proxy_stream/' + stateObj.entityId +
return '/api/camera_proxy_stream/' + stateObj.entity_id +
'?token=' + stateObj.attributes.access_token;
}
// Return an empty image if no stateObj (= dialog not open) or in cleanup mode.

View File

@ -376,9 +376,9 @@ Polymer({
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
/* eslint-disable no-param-reassign */
data.entity_id = this.stateObj.entityId;
data.entity_id = this.stateObj.entity_id;
/* eslint-enable no-param-reassign */
this.hass.serviceActions.callService('climate', service, data)
this.hass.callService('climate', service, data)
.then(function () {
this.stateObjChanged(this.stateObj);
}.bind(this));

View File

@ -7,8 +7,6 @@
<link rel='import' href='../../bower_components/paper-button/paper-button.html'>
<link rel='import' href='../../bower_components/paper-input/paper-input-container.html'>
<link rel='import' href='../util/hass-behavior.html'>
<dom-module id='more-info-configurator'>
<template>
<style is="custom-style" include="iron-flex"></style>
@ -94,8 +92,6 @@
Polymer({
is: 'more-info-configurator',
behaviors: [window.hassBehavior],
properties: {
stateObj: {
type: Object,
@ -106,13 +102,6 @@ Polymer({
value: 'display',
},
isStreaming: {
type: Boolean,
bindNuclear: function (hass) {
return hass.streamGetters.isStreamingEvents;
},
},
isConfigurable: {
type: Boolean,
computed: 'computeIsConfigurable(stateObj)',
@ -154,13 +143,9 @@ Polymer({
this.isConfiguring = true;
this.hass.serviceActions.callService('configurator', 'configure', data).then(
this.hass.callService('configurator', 'configure', data).then(
function () {
this.isConfiguring = false;
if (!this.isStreaming) {
this.hass.syncActions.fetchAll();
}
}.bind(this),
function () {
this.isConfiguring = false;

View File

@ -84,15 +84,15 @@ Polymer({
},
coverPositionSliderChanged: function (ev) {
this.hass.serviceActions.callService('cover', 'set_cover_position', {
entity_id: this.stateObj.entityId,
this.hass.callService('cover', 'set_cover_position', {
entity_id: this.stateObj.entity_id,
position: ev.target.value,
});
},
coverTiltPositionSliderChanged: function (ev) {
this.hass.serviceActions.callService('cover', 'set_cover_tilt_position', {
entity_id: this.stateObj.entityId,
this.hass.callService('cover', 'set_cover_tilt_position', {
entity_id: this.stateObj.entity_id,
tilt_position: ev.target.value,
});
},
@ -106,18 +106,18 @@ Polymer({
},
onOpenTiltTap: function () {
this.hass.serviceActions.callService('cover', 'open_cover_tilt',
{ entity_id: this.stateObj.entityId });
this.hass.callService('cover', 'open_cover_tilt',
{ entity_id: this.stateObj.entity_id });
},
onCloseTiltTap: function () {
this.hass.serviceActions.callService('cover', 'close_cover_tilt',
{ entity_id: this.stateObj.entityId });
this.hass.callService('cover', 'close_cover_tilt',
{ entity_id: this.stateObj.entity_id });
},
onStopTiltTap: function () {
this.hass.serviceActions.callService('cover', 'stop_cover_tilt',
{ entity_id: this.stateObj.entityId });
this.hass.callService('cover', 'stop_cover_tilt',
{ entity_id: this.stateObj.entity_id });
},
});
</script>

View File

@ -114,8 +114,8 @@ Polymer({
speedInput = this.stateObj.attributes.speed_list[speedIndex];
if (speedInput === this.stateObj.attributes.speed) return;
this.hass.serviceActions.callService('fan', 'turn_on', {
entity_id: this.stateObj.entityId,
this.hass.callService('fan', 'turn_on', {
entity_id: this.stateObj.entity_id,
speed: speedInput,
});
},
@ -126,22 +126,22 @@ Polymer({
if (oldVal === newVal) return;
this.hass.serviceActions.callService('fan', 'oscillate', {
entity_id: this.stateObj.entityId,
this.hass.callService('fan', 'oscillate', {
entity_id: this.stateObj.entity_id,
oscillating: newVal,
});
},
onDirectionLeft: function () {
this.hass.serviceActions.callService('fan', 'set_direction', {
entity_id: this.stateObj.entityId,
this.hass.callService('fan', 'set_direction', {
entity_id: this.stateObj.entity_id,
direction: 'left'
});
},
onDirectionRight: function () {
this.hass.serviceActions.callService('fan', 'set_direction', {
entity_id: this.stateObj.entityId,
this.hass.callService('fan', 'set_direction', {
entity_id: this.stateObj.entity_id,
direction: 'right'
});
},

View File

@ -2,8 +2,6 @@
<link rel="import" href="../state-summary/state-card-content.html">
<link rel='import' href='../util/hass-behavior.html'>
<dom-module id="more-info-group">
<template>
<style>
@ -29,8 +27,6 @@
Polymer({
is: 'more-info-group',
behaviors: [window.hassBehavior],
properties: {
hass: {
type: Object,
@ -42,20 +38,7 @@ Polymer({
states: {
type: Array,
bindNuclear: function (hass) {
return [
hass.moreInfoGetters.currentEntity,
hass.entityGetters.entityMap,
function (currentEntity, entities) {
// weird bug??
if (!currentEntity) {
return [];
}
return currentEntity.attributes.entity_id.map(
entities.get.bind(entities));
},
];
},
computed: 'computeStates(stateObj, hass)',
},
},
@ -63,6 +46,21 @@ Polymer({
'statesChanged(stateObj, states)',
],
computeStates: function (stateObj, hass) {
var states = [];
var entIds = stateObj.attributes.entity_id;
for (var i = 0; i < entIds.length; i++) {
var state = hass.states[entIds[i]];
if (state) {
states.push(state);
}
}
return states;
},
statesChanged: function (stateObj, states) {
var groupDomainStateObj = false;
var baseStateObj;
@ -73,15 +71,18 @@ Polymer({
if (states && states.length > 0) {
baseStateObj = states[0];
groupDomainStateObj = baseStateObj.set('entityId', stateObj.entityId).set(
'attributes', Object.assign({}, baseStateObj.attributes));
groupDomainStateObj = Object.assign(baseStateObj, {
entity_id: stateObj.entity_id,
attributes: Object.assign({}, baseStateObj.attributes)
});
var groupDomain = window.hassUtil.computeDomain(groupDomainStateObj);
for (i = 0; i < states.length; i++) {
state = states[i];
if (state && state.domain) {
if (groupDomainStateObj.domain !== state.domain) {
groupDomainStateObj = false;
}
if (groupDomain !== window.hassUtil.computeDomain(state)) {
groupDomainStateObj = false;
break;
}
}
}

View File

@ -165,8 +165,8 @@ Polymer({
effectInput = this.stateObj.attributes.effect_list[effectIndex];
if (effectInput === this.stateObj.attributes.effect) return;
this.hass.serviceActions.callService('light', 'turn_on', {
entity_id: this.stateObj.entityId,
this.hass.callService('light', 'turn_on', {
entity_id: this.stateObj.entity_id,
effect: effectInput,
});
},
@ -177,10 +177,12 @@ Polymer({
if (isNaN(bri)) return;
if (bri === 0) {
this.hass.serviceActions.callTurnOff(this.stateObj.entityId);
this.hass.callService('light', 'turn_off', {
entity_id: this.stateObj.entity_id,
});
} else {
this.hass.serviceActions.callService('light', 'turn_on', {
entity_id: this.stateObj.entityId,
this.hass.callService('light', 'turn_on', {
entity_id: this.stateObj.entity_id,
brightness: bri,
});
}
@ -191,8 +193,8 @@ Polymer({
if (isNaN(ct)) return;
this.hass.serviceActions.callService('light', 'turn_on', {
entity_id: this.stateObj.entityId,
this.hass.callService('light', 'turn_on', {
entity_id: this.stateObj.entity_id,
color_temp: ct,
});
},
@ -202,14 +204,14 @@ Polymer({
if (isNaN(wv)) return;
this.hass.serviceActions.callService('light', 'turn_on', {
entity_id: this.stateObj.entityId,
this.hass.callService('light', 'turn_on', {
entity_id: this.stateObj.entity_id,
white_value: wv,
});
},
serviceChangeColor: function (hass, entityId, color) {
hass.serviceActions.callService('light', 'turn_on', {
hass.callService('light', 'turn_on', {
entity_id: entityId,
rgb_color: [color.r, color.g, color.b],
});
@ -227,14 +229,14 @@ Polymer({
this.color = ev.detail.rgb;
this.serviceChangeColor(this.hass, this.stateObj.entityId, this.color);
this.serviceChangeColor(this.hass, this.stateObj.entity_id, this.color);
this.colorChanged = false;
this.skipColorPicked = true;
this.colorDebounce = setTimeout(function () {
if (this.colorChanged) {
this.serviceChangeColor(this.hass, this.stateObj.entityId, this.color);
this.serviceChangeColor(this.hass, this.stateObj.entity_id, this.color);
}
this.skipColorPicked = false;
}.bind(this), 500);

View File

@ -58,8 +58,8 @@ Polymer({
callService: function (service, data) {
var serviceData = data || {};
serviceData.entity_id = this.stateObj.entityId;
this.hass.serviceActions.callService('lock', service, serviceData);
serviceData.entity_id = this.stateObj.entity_id;
this.hass.callService('lock', service, serviceData);
},
});
</script>

View File

@ -10,8 +10,6 @@
<link rel='import' href='../../bower_components/paper-menu/paper-menu.html'>
<link rel='import' href='../../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../util/hass-behavior.html'>
<dom-module id='more-info-media_player'>
<template>
<style is="custom-style" include="iron-flex iron-flex-alignment"></style>
@ -115,14 +113,10 @@
Polymer({
is: 'more-info-media_player',
behaviors: [window.hassBehavior],
properties: {
ttsLoaded: {
type: Boolean,
bindNuclear: function (hass) {
return hass.configGetters.isComponentLoaded('tts');
},
computed: 'computeTTSLoaded(hass)',
},
hass: {
@ -307,6 +301,10 @@ Polymer({
return !ttsLoaded || !supportsPlayMedia;
},
computeTTSLoaded: function (hass) {
return window.hassUtil.isComponentLoaded(hass, 'tts');
},
handleTogglePower: function () {
this.callService(this.isOff ? 'turn_on' : 'turn_off');
},
@ -376,15 +374,14 @@ Polymer({
},
sendTTS: function () {
var services = this.hass.reactor.evaluate(
this.hass.serviceGetters.entityMap).get('tts').get('services').keySeq()
.toArray();
var services = this.hass.config.services.tts;
var serviceKeys = Object.keys(services).sort();
var service;
var i;
for (i = 0; i < services.length; i++) {
if (services[i].indexOf('_say') !== -1) {
service = services[i];
for (i = 0; i < serviceKeys.length; i++) {
if (services[serviceKeys[i]].indexOf('_say') !== -1) {
service = services[serviceKeys[i]];
break;
}
}
@ -393,8 +390,8 @@ Polymer({
return;
}
this.hass.serviceActions.callService('tts', service, {
entity_id: this.stateObj.entityId,
this.hass.callService('tts', service, {
entity_id: this.stateObj.entity_id,
message: this.ttsMessage,
});
this.ttsMessage = '';
@ -403,8 +400,8 @@ Polymer({
callService: function (service, data) {
var serviceData = data || {};
serviceData.entity_id = this.stateObj.entityId;
this.hass.serviceActions.callService('media_player', service, serviceData);
serviceData.entity_id = this.stateObj.entity_id;
this.hass.callService('media_player', service, serviceData);
},
});
</script>

View File

@ -36,12 +36,12 @@ Polymer({
],
inputChanged: function (hass, inDialog, stateObj) {
if (!stateObj) return;
if (!stateObj || !hass) return;
window.hassUtil.dynamicContentUpdater(
this,
('STATE-CARD-' +
window.hassUtil.stateCardType(this.hass, stateObj).toUpperCase()),
window.hassUtil.stateCardType(hass, stateObj).toUpperCase()),
{
hass: hass,
stateObj: stateObj,

View File

@ -68,18 +68,18 @@ Polymer({
},
onOpenTap: function () {
this.hass.serviceActions.callService('cover', 'open_cover',
{ entity_id: this.stateObj.entityId });
this.hass.callService('cover', 'open_cover',
{ entity_id: this.stateObj.entity_id });
},
onCloseTap: function () {
this.hass.serviceActions.callService('cover', 'close_cover',
{ entity_id: this.stateObj.entityId });
this.hass.callService('cover', 'close_cover',
{ entity_id: this.stateObj.entity_id });
},
onStopTap: function () {
this.hass.serviceActions.callService('cover', 'stop_cover',
{ entity_id: this.stateObj.entityId });
this.hass.callService('cover', 'stop_cover',
{ entity_id: this.stateObj.entity_id });
},
});
</script>

View File

@ -20,7 +20,7 @@
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]" in-dialog='[[inDialog]]'></state-info>
<div class='state'>[[stateObj.stateDisplay]]</div>
<div class='state'>[[computeStateDisplay(stateObj)]]</div>
</div>
</template>
</dom-module>
@ -39,5 +39,9 @@ Polymer({
type: Object,
},
},
computeStateDisplay: function (stateObj) {
return window.hassUtil.computeStateState(stateObj);
},
});
</script>

View File

@ -27,7 +27,7 @@
<paper-dropdown-menu
on-tap='stopPropagation'
selected-item-label='{{selectedOption}}'
label='[[stateObj.entityDisplay]]'>
label='[[computeStateName(stateObj)]]'>
<paper-menu class="dropdown-content" selected="[[computeSelected(stateObj)]]">
<template is='dom-repeat' items='[[stateObj.attributes.options]]'>
<paper-item>[[item]]</paper-item>
@ -61,6 +61,10 @@ Polymer({
},
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
},
computeSelected: function (stateObj) {
return stateObj.attributes.options.indexOf(stateObj.state);
},
@ -70,9 +74,9 @@ Polymer({
if (option === '' || option === this.stateObj.state) {
return;
}
this.hass.serviceActions.callService('input_select', 'select_option', {
this.hass.callService('input_select', 'select_option', {
option: option,
entity_id: this.stateObj.entityId,
entity_id: this.stateObj.entity_id,
});
},

View File

@ -68,9 +68,9 @@ Polymer({
if (this.value === Number(this.stateObj.state)) {
return;
}
this.hass.serviceActions.callService('input_slider', 'select_value', {
this.hass.callService('input_slider', 'select_value', {
value: this.value,
entity_id: this.stateObj.entityId,
entity_id: this.stateObj.entity_id,
});
},

View File

@ -38,8 +38,8 @@
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]" in-dialog='[[inDialog]]'></state-info>
<div class='state'>
<div class='main-text' take-height$='[[!secondaryText]]'>[[computePrimaryText(stateObj, isPlaying)]]</div>
<div class='secondary-text'>[[secondaryText]]</div>
<div class='main-text' take-height$='[[!playerObj.secondaryText]]'>[[playerObj.primaryText]]</div>
<div class='secondary-text'>[[playerObj.secondaryText]]</div>
</div>
</div>
</template>
@ -51,6 +51,10 @@ Polymer({
is: 'state-card-media_player',
properties: {
hass: {
type: Object,
},
inDialog: {
type: Boolean,
value: false,
@ -60,41 +64,14 @@ Polymer({
type: Object,
},
isPlaying: {
type: Boolean,
computed: 'computeIsPlaying(stateObj)',
},
secondaryText: {
type: String,
computed: 'computeSecondaryText(stateObj)',
playerObj: {
type: Object,
computed: 'computePlayerObj(hass, stateObj)',
},
},
computeIsPlaying: function (stateObj) {
return this.PLAYING_STATES.indexOf(stateObj.state) !== -1;
},
computePrimaryText: function (stateObj, isPlaying) {
return isPlaying ? stateObj.attributes.media_title : stateObj.stateDisplay;
},
computeSecondaryText: function (stateObj) {
var text;
if (stateObj.attributes.media_content_type === 'music') {
return stateObj.attributes.media_artist;
} else if (stateObj.attributes.media_content_type === 'tvshow') {
text = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season && stateObj.attributes.media_episode) {
text += ' S' + stateObj.attributes.media_season + 'E' + stateObj.attributes.media_episode;
}
return text;
} else if (stateObj.attributes.app_name) {
return stateObj.attributes.app_name;
}
return '';
computePlayerObj: function (hass, stateObj) {
return new window.MediaPlayerEntity(hass, stateObj);
},
});
</script>

View File

@ -47,7 +47,9 @@ Polymer({
activateScene: function (ev) {
ev.stopPropagation();
this.hass.serviceActions.callTurnOn(this.stateObj.entityId);
this.hass.callService(
'scene', 'turn_on',
{ entity_id: this.stateObj.entity_id });
},
});
</script>

View File

@ -53,7 +53,9 @@ Polymer({
fireScript: function (ev) {
ev.stopPropagation();
this.hass.serviceActions.callTurnOn(this.stateObj.entityId);
this.hass.callService(
'script', 'turn_on',
{ entity_id: this.stateObj.entity_id });
},
});
</script>

View File

@ -20,7 +20,7 @@
</style>
<state-badge state-obj='[[stateObj]]' in-dialog='[[inDialog]]'></state-badge>
<a href$='[[stateObj.state]]' target='_blank' class='name' id='link'>[[stateObj.entityDisplay]]</a>
<a href$='[[stateObj.state]]' target='_blank' class='name' id='link'>[[computeStateName(stateObj)]]</a>
</template>
</dom-module>
@ -43,6 +43,10 @@ Polymer({
tap: 'onTap',
},
computeStateName: function (stateObj) {
return window.hassUtil.computeStateName(stateObj);
},
onTap: function (ev) {
ev.stopPropagation();
if (ev.target === this.$.link) {

View File

@ -0,0 +1,48 @@
<script>
(function () {
var STORED_STATE = [
'dockedSidebar',
];
Polymer({
is: 'ha-pref-storage',
properties: {
hass: {
type: Object,
},
storage: {
type: Object,
value: window.localStorage || {},
},
},
storeState: function () {
if (!this.hass) return;
try {
for (var i = 0; i < STORED_STATE.length; i++) {
var key = STORED_STATE[i];
this.storage[key] = JSON.stringify(this.hass[key]);
}
} catch (err) {
// Safari throws exception in private mode
}
},
getStoredState: function () {
var state = {};
for (var i = 0; i < STORED_STATE.length; i++) {
var key = STORED_STATE[i];
if (key in this.storage) {
state[key] = JSON.parse(this.storage[key]);
}
}
return state;
}
});
}());
</script>

89
src/util/ha-url-sync.html Normal file
View File

@ -0,0 +1,89 @@
<script>
(function () {
var PAGE_TITLE = 'Home Assistant';
function pageState(panel, view) {
var state = { panel: panel };
if (panel === 'states') {
state.view = view || null;
}
return state;
}
function pageUrl(pane, view) {
return pane === 'states' && view ?
'/' + pane + '/' + view : '/' + pane;
}
Polymer({
is: 'ha-url-sync',
properties: {
hass: {
type: Object,
observer: 'hassChanged',
},
},
hassChanged: function (newHass, oldHass) {
if (!oldHass) {
return;
} else if (newHass.currentPanel === oldHass.currentPanel &&
newHass.currentView === oldHass.currentView) {
// did the more info entity change?
if (oldHass.moreInfoEntityId !== newHass.moreInfoEntityId) {
if (newHass.moreInfoEntityId) {
// push same state so that back button works.
history.pushState(history.state, PAGE_TITLE, window.location.pathname);
} else if (this.ignoreNextDeselectEntity) {
this.ignoreNextDeselectEntity = false;
} else {
history.back();
}
}
return;
} else if (this.ignoreNextNav) {
this.ignoreNextNav = false;
return;
}
history.pushState(
pageState(newHass.currentPanel, newHass.currentView), PAGE_TITLE,
pageUrl(newHass.currentPanel, newHass.currentView));
},
popstateChangeListener: function (ev) {
if (this.hass.moreInfoEntityId) {
this.ignoreNextDeselectEntity = true;
this.fire('hass-more-info', { entityId: null });
} else if (this.hass.currentPanel !== ev.state.panel ||
this.hass.currentView !== ev.state.view) {
this.ignoreNextNav = true;
this.fire('hass-navigate', ev.state);
}
},
// initial url sync
attached: function () {
this.popstateChangeListener = this.popstateChangeListener.bind(this);
// keep state in sync when url changes via forward/back buttons
window.addEventListener('popstate', this.popstateChangeListener);
// store current view / panel
if (window.location.pathname === '/') {
var currentPanel = this.hass.currentPanel;
var currentView = this.hass.currentView;
history.replaceState(
pageState(currentPanel, currentView), PAGE_TITLE,
pageUrl(currentPanel, currentView));
} else {
var parts = window.location.pathname.substr(1).split('/');
this.fire('hass-navigate', pageState(parts[0], parts[1]));
}
}
});
}());
</script>

View File

@ -1,40 +0,0 @@
<script>
/** @polymerBehavior */
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) {
var getter;
if (!('bindNuclear' in this.properties[key])) {
return unwatchFns;
}
getter = this.properties[key].bindNuclear(hass);
if (!getter) {
throw new Error('Undefined getter specified for key ' + key +
' on ' + this.nodeName);
}
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

@ -0,0 +1,81 @@
<script>
window.hassCallApi = function (host, auth, method, path, parameters) {
var url = host + '/api/' + path;
if (window.HASS_DEMO) {
var component = path.split('/', 1)[0];
var data;
switch (component) {
case 'bootstrap':
data = window.hassDemoData.bootstrap;
break;
case 'logbook':
data = window.hassDemoData.logbook;
break;
case 'history':
data = window.hassDemoData.stateHistory;
break;
default:
data = false;
}
return new Promise(function (resolve, reject) {
if (data) {
resolve(data);
} else {
reject('Request not allowed in demo mode.');
}
});
}
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open(method, url, true);
if (auth.authToken) {
req.setRequestHeader('X-HA-access', auth.authToken);
}
req.onload = function () {
var body = req.responseText;
if (req.getResponseHeader('content-type') === 'application/json') {
try {
body = JSON.parse(req.responseText);
} catch (err) {
reject({
error: 'Unable to parse JSON response',
status_code: req.status,
body: body,
});
return;
}
}
if (req.status > 199 && req.status < 300) {
resolve(body);
} else {
reject({
error: 'Response error: ' + req.status,
status_code: req.status,
body: body
});
}
};
req.onerror = function () {
reject({
error: 'Request error',
status_code: req.status,
body: req.responseText,
});
};
if (parameters) {
req.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
req.send(JSON.stringify(parameters));
} else {
req.send();
}
});
};
</script>

View File

@ -46,8 +46,28 @@ window.hassUtil.attributeClassNames = function (stateObj, attributes) {
).join(' ');
};
window.hassUtil.canToggle = function (hass, entityId) {
return hass.reactor.evaluate(hass.serviceGetters.canToggleEntity(entityId));
window.hassUtil.canToggleState = function (hass, stateObj) {
var domain = window.hassUtil.computeDomain(stateObj);
if (domain === 'group') {
return stateObj.state === 'on' || stateObj.state === 'off';
}
return window.hassUtil.canToggleDomain(hass, domain);
};
window.hassUtil.canToggleDomain = function (hass, domain) {
var turnOnService;
var services = hass.config.services[domain];
if (domain === 'lock') {
turnOnService = 'lock';
} else if (domain === 'cover') {
turnOnService = 'open_cover';
} else {
turnOnService = 'turn_on';
}
return services && turnOnService in services;
};
// Update root's child element to be newElementTag replacing another existing child if any.
@ -168,22 +188,29 @@ window.hassUtil.relativeTime.tests = [
7, 'day',
];
window.hassUtil.stateCardType = function (hass, state) {
if (state.state === 'unavailable') {
window.hassUtil.stateCardType = function (hass, stateObj) {
if (stateObj.state === 'unavailable') {
return 'display';
} else if (window.hassUtil.DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) {
return state.domain;
} else if (window.hassUtil.canToggle(hass, state.entityId) && state.attributes.control !== 'hidden') {
}
var domain = window.hassUtil.computeDomain(stateObj);
if (window.hassUtil.DOMAINS_WITH_CARD.indexOf(domain) !== -1) {
return domain;
} else if (window.hassUtil.canToggleState(hass, stateObj) &&
stateObj.attributes.control !== 'hidden') {
return 'toggle';
}
return 'display';
};
window.hassUtil.stateMoreInfoType = function (state) {
if (window.hassUtil.DOMAINS_WITH_MORE_INFO.indexOf(state.domain) !== -1) {
return state.domain;
window.hassUtil.stateMoreInfoType = function (stateObj) {
var domain = window.hassUtil.computeDomain(stateObj);
if (window.hassUtil.DOMAINS_WITH_MORE_INFO.indexOf(domain) !== -1) {
return domain;
}
if (window.hassUtil.HIDE_MORE_INFO.indexOf(state.domain) !== -1) {
if (window.hassUtil.HIDE_MORE_INFO.indexOf(domain) !== -1) {
return 'hidden';
}
return 'default';
@ -318,27 +345,65 @@ window.hassUtil.binarySensorIcon = function (state) {
};
window.hassUtil.stateIcon = function (state) {
var unit;
if (!state) {
return window.hassUtil.DEFAULT_ICON;
} else if (state.attributes.icon) {
return state.attributes.icon;
}
unit = state.attributes.unit_of_measurement;
var unit = state.attributes.unit_of_measurement;
var domain = window.hassUtil.computeDomain(state);
if (unit && state.domain === 'sensor') {
if (unit && domain === 'sensor') {
if (unit === '°C' || unit === '°F') {
return 'mdi:thermometer';
} else if (unit === 'Mice') {
return 'mdi:mouse-variant';
}
} else if (state.domain === 'binary_sensor') {
} else if (domain === 'binary_sensor') {
return window.hassUtil.binarySensorIcon(state);
}
return window.hassUtil.domainIcon(state.domain, state.state);
return window.hassUtil.domainIcon(domain, state.state);
};
window.hassUtil.computeDomain = function (stateObj) {
if (!stateObj._domain) {
stateObj._domain = window.HAWS.extractDomain(stateObj.entity_id);
}
return stateObj._domain;
};
window.hassUtil.computeStateName = function (stateObj) {
if (!stateObj._entityDisplay) {
stateObj._entityDisplay = (
stateObj.attributes.friendly_name ||
window.HAWS.extractObjectId(stateObj.entity_id)
.replace(/_/g, ' '));
}
return stateObj._entityDisplay;
};
window.hassUtil.computeStateState = function (stateObj) {
if (!stateObj._stateDisplay) {
stateObj._stateDisplay = stateObj.state.replace(/_/g, ' ');
if (stateObj.attributes.unit_of_measurement) {
stateObj._stateDisplay += ` ${stateObj.attributes.unit_of_measurement}`;
}
}
return stateObj._stateDisplay;
};
window.hassUtil.isComponentLoaded = function (hass, component) {
return hass.config.core.components.indexOf(component) !== -1;
};
window.hassUtil.computeLocationName = function (hass) {
return hass.config.core.location_name;
};
</script>

View File

@ -0,0 +1,195 @@
<script>
(function () {
window.MediaPlayerEntity = function (hass, stateObj) {
this.hass = hass;
this.stateObj = stateObj;
};
function addGetter(name, getter) {
Object.defineProperty(window.MediaPlayerEntity.prototype, name,
{ get: getter });
}
addGetter('isOff', function () {
return this.stateObj.state === 'off';
});
addGetter('isIdle', function () {
return this.stateObj.state === 'idle';
});
addGetter('isMuted', function () {
return this.stateObj.attributes.is_volume_muted;
});
addGetter('isPaused', function () {
return this.stateObj.state === 'paused';
});
addGetter('isPlaying', function () {
return this.stateObj.state === 'playing';
});
addGetter('isMusic', function () {
return this.stateObj.attributes.media_content_type === 'music';
});
addGetter('isTVShow', function () {
return this.stateObj.attributes.media_content_type === 'tvshow';
});
addGetter('hasMediaControl', function () {
return ['playing', 'paused', 'unknown'].indexOf(
this.stateObj.state) !== -1;
});
addGetter('volumeSliderValue', function () {
return this.stateObj.attributes.volume_level * 100;
});
addGetter('showProgress', function () {
return (
(this.isPlaying || this.isPaused) &&
'media_position' in this.stateObj.attributes &&
'media_position_updated_at' in this.stateObj.attributes);
});
addGetter('currentProgress', function () {
return (
this.stateObj.attributes.media_position +
((Date.now() -
new Date(this.stateObj.attributes.media_position_updated_at)) / 1000));
});
/* eslint-disable no-bitwise */
addGetter('supportsPause', function () {
return (this.stateObj.attributes.supported_media_commands & 1) !== 0;
});
addGetter('supportsVolumeSet', function () {
return (this.stateObj.attributes.supported_media_commands & 4) !== 0;
});
addGetter('supportsVolumeMute', function () {
return (this.stateObj.attributes.supported_media_commands & 8) !== 0;
});
addGetter('supportsPreviousTrack', function () {
return (this.stateObj.attributes.supported_media_commands & 16) !== 0;
});
addGetter('supportsNextTrack', function () {
return (this.stateObj.attributes.supported_media_commands & 32) !== 0;
});
addGetter('supportsTurnOn', function () {
return (this.stateObj.attributes.supported_media_commands & 128) !== 0;
});
addGetter('supportsTurnOff', function () {
return (this.stateObj.attributes.supported_media_commands & 256) !== 0;
});
addGetter('supportsPlayMedia', function () {
return (this.stateObj.attributes.supported_media_commands & 512) !== 0;
});
addGetter('supportsVolumeButtons', function () {
return (this.stateObj.attributes.supported_media_commands & 1024) !== 0;
});
addGetter('supportsPlay', function () {
return (this.stateObj.attributes.supported_media_commands & 16384) !== 0;
});
/* eslint-enable no-bitwise */
addGetter('primaryText', function () {
return this.stateObj.attributes.media_title ||
window.hassUtil.computeStateState(this.stateObj);
});
addGetter('secondaryText', function () {
if (this.isMusic) {
return this.stateObj.attributes.media_artist;
} else if (this.isTVShow) {
var text = this.stateObj.attributes.media_series_title;
if (this.stateObj.attributes.media_season) {
text += ' S' + this.stateObj.attributes.media_season;
if (this.stateObj.attributes.media_episode) {
text += 'E' + this.stateObj.attributes.media_episode;
}
}
return text;
} else if (this.stateObj.attributes.app_name) {
return this.stateObj.attributes.app_name;
}
return '';
});
Object.assign(window.MediaPlayerEntity.prototype, {
mediaPlayPause() {
this.callService('media_play_pause');
},
nextTrack() {
this.callService('media_next_track');
},
playbackControl() {
this.callService('media_play_pause');
},
previousTrack() {
this.callService('media_previous_track');
},
setVolume(volume) {
this.callService('volume_set', { volume_level: volume });
},
togglePower() {
if (this.isOff) {
this.turnOn();
} else {
this.turnOff();
}
},
turnOff() {
this.callService('turn_off');
},
turnOn() {
this.callService('turn_on');
},
volumeDown() {
this.callService('volume_down');
},
volumeMute(mute) {
if (!this.supportsVolumeMute) {
throw new Error('Muting volume not supported');
}
this.callService('volume_mute', { is_volume_muted: mute });
},
volumeUp() {
this.callService('volume_down');
},
// helper method
callService(service, data) {
var serviceData = data || {};
serviceData.entity_id = this.stateObj.entity_id;
this.hass.callService('media_player', service, serviceData);
},
});
}());
</script>

View File

@ -905,10 +905,6 @@ class-extend@^0.1.0, class-extend@^0.1.1:
dependencies:
object-assign "^2.0.0"
classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
clean-css@3.4.x:
version "3.4.24"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.24.tgz#89f5a5e9da37ae02394fe049a41388abbe72c3b5"
@ -2806,9 +2802,9 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
home-assistant-js-websocket@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-0.5.0.tgz#f78b31d160d7bdd083cf53ae01328483e7046ef5"
home-assistant-js-websocket@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-0.7.3.tgz#e65ac99b59a0b2c623048457c259808d67030c7f"
homedir-polyfill@^1.0.0:
version "1.0.1"
@ -2934,10 +2930,6 @@ ignore@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435"
immutable@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@ -3336,10 +3328,6 @@ keep-alive-agent@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/keep-alive-agent/-/keep-alive-agent-0.0.1.tgz#44847ca394ce8d6b521ae85816bd64509942b385"
keymirror@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35"
kind-of@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47"
@ -3997,12 +3985,6 @@ nth-check@~1.0.0:
dependencies:
boolbase "~1.0.0"
nuclear-js@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/nuclear-js/-/nuclear-js-1.4.0.tgz#6c9c001b0673f0ae9d8f8b188c4da04ed693a7be"
dependencies:
immutable "^3.8.1"
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@ -4027,7 +4009,7 @@ object-assign@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -5535,9 +5517,9 @@ ternary-stream@^2.0.1:
merge-stream "^1.0.0"
through2 "^2.0.1"
"test-fixture@github:polymerelements/test-fixture":
test-fixture@PolymerElements/test-fixture:
version "2.0.1"
resolved "https://codeload.github.com/polymerelements/test-fixture/tar.gz/f72f0ff4e83b83b0157afed664f560da91d20f31"
resolved "https://codeload.github.com/PolymerElements/test-fixture/tar.gz/f72f0ff4e83b83b0157afed664f560da91d20f31"
test-value@^1.1.0:
version "1.1.0"