Frontend now build on top of home-assistant-js

This commit is contained in:
Paulus Schoutsen 2015-02-03 23:16:53 -08:00
parent fbae2ef725
commit 115be859b6
36 changed files with 467 additions and 829 deletions

3
.gitmodules vendored
View file

@ -10,3 +10,6 @@
[submodule "homeassistant/external/noop"]
path = homeassistant/external/noop
url = https://github.com/balloob/noop.git
[submodule "homeassistant/components/frontend/www_static/polymer/home-assistant-js"]
path = homeassistant/components/frontend/www_static/polymer/home-assistant-js
url = https://github.com/balloob/home-assistant-js.git

View file

@ -58,7 +58,7 @@ def _handle_get_root(handler, path_match, data):
handler.end_headers()
if handler.server.development:
app_url = "polymer/splash-login.html"
app_url = "polymer/home-assistant.html"
else:
app_url = "frontend-{}.html".format(version.VERSION)
@ -83,7 +83,7 @@ def _handle_get_root(handler, path_match, data):
"<script"
" src='/static/webcomponents.min.js'></script>"
"<link rel='import' href='/static/{}' />"
"<splash-login auth='{}'></splash-login>"
"<home-assistant auth='{}'></home-assistant>"
"</body></html>").format(app_url, auth))

View file

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "954620894f13782f17ae7443f0df4ffc"
VERSION = "dae175210dab23f9c383a593d8e97d9b"

File diff suppressed because one or more lines are too long

View file

@ -34,7 +34,6 @@
"paper-dropdown": "polymer/paper-dropdown#~0.5.4",
"paper-item": "polymer/paper-item#~0.5.4",
"paper-slider": "polymer/paper-slider#~0.5.4",
"moment": "~2.8.4",
"color-picker-element": "~0.0.2",
"google-apis": "GoogleWebComponents/google-apis#~0.4.2"
}

View file

@ -1,4 +1,3 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">

View file

@ -5,7 +5,7 @@
<link rel="import" href="state-card-thermostat.html">
<link rel="import" href="state-card-configurator.html">
<polymer-element name="state-card-content" attributes="api stateObj">
<polymer-element name="state-card-content" attributes="stateObj">
<template>
<style>
:host {

View file

@ -1,4 +1,3 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">

View file

@ -1,4 +1,3 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">

View file

@ -1,4 +1,3 @@
<script src="../bower_components/moment/moment.js"></script>
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
@ -38,6 +37,10 @@
'stateObj.state': 'stateChanged'
},
ready: function() {
this.forceStateChange = this.forceStateChange.bind(this);
},
// prevent the event from propegating
toggleClicked: function(ev) {
ev.stopPropagation();
@ -60,16 +63,17 @@
this.toggleChecked = newVal === "on";
},
forceStateChange: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
},
turn_on: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
this.api.turn_on(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
window.hass.serviceActions.callTurnOn(this.stateObj.entity_id)
.then(this.forceStateChange);
},
turn_off: function() {
@ -77,11 +81,8 @@
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
this.api.turn_off(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
window.hass.serviceActions.callTurnOff(this.stateObj.entity_id)
.then(this.forceStateChange);
},
});

View file

@ -25,7 +25,7 @@
Polymer({
cardClicked: function() {
this.api.showmoreInfoDialog(this.stateObj.entity_id);
window.hass.uiActions.showMoreInfoDialog(this.stateObj.entity_id);
},
});

View file

@ -35,13 +35,18 @@
cbEventClicked: null,
states: [],
domReady: function() {
this.api.addEventListener('states-updated', this.statesUpdated.bind(this))
this.statesUpdated()
ready: function() {
this.statesUpdated = this.statesUpdated.bind(this);
window.hass.stateStore.addChangeListener(this.statesUpdated);
this.statesUpdated();
},
detached: function() {
window.hass.stateStore.removeChangeListener(this.statesUpdated);
},
statesUpdated: function() {
this.states = this.api.states;
this.states = window.hass.stateStore.all();
},
handleClick: function(ev) {

View file

@ -1,6 +1,6 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="events-list" attributes="api cbEventClicked">
<polymer-element name="events-list" attributes="cbEventClicked">
<template>
<style>
:host {
@ -37,14 +37,18 @@
cbEventClicked: null,
events: [],
domReady: function() {
this.events = this.api.events
ready: function() {
this.eventsUpdated = this.eventsUpdated.bind(this);
window.hass.eventStore.addChangeListener(this.eventsUpdated);
this.eventsUpdated();
},
this.api.addEventListener('events-updated', this.eventsUpdated.bind(this))
detached: function() {
window.hass.eventStore.removeChangeListener(this.eventsUpdated);
},
eventsUpdated: function() {
this.events = this.api.events;
this.events = window.hass.eventStore.all();
},
handleClick: function(ev) {

View file

@ -2,7 +2,7 @@
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="./loading-box.html">
<polymer-element name="recent-states" attributes="api stateObj">
<polymer-element name="recent-states" attributes="stateObj">
<template>
<core-style ref='ha-data-table'></core-style>
@ -36,13 +36,12 @@
stateObjChanged: function() {
this.recentStates = null;
this.api.call_api('GET', 'history/entity/' + this.stateObj.entity_id + '/recent_states', {}, this.newStates.bind(this));
window.hass.callApi(
'GET', 'history/entity/' + this.stateObj.entity_id + '/recent_states').then(
function(states) {
this.recentStates = states.slice(1);
}.bind(this));
},
newStates: function(states) {
// cut off the first one (which is the current)
this.recentStates = states.slice(1);
}
});
</script>
</polymer-element>

View file

@ -51,18 +51,22 @@
services: [],
cbServiceClicked: null,
domReady: function() {
this.services = this.api.services
ready: function() {
this.servicesUpdated = this.servicesUpdated.bind(this);
window.hass.serviceStore.addChangeListener(this.servicesUpdated);
this.servicesUpdated();
},
this.api.addEventListener('services-updated', this.servicesUpdated.bind(this))
detached: function() {
window.hass.serviceStore.removeChangeListener(this.servicesUpdated);
},
getIcon: function(domain) {
return (new DomainIcon).icon(domain);
return (new DomainIcon()).icon(domain);
},
servicesUpdated: function() {
this.services = this.api.services;
this.services = window.hass.serviceStore.all();
},
serviceClicked: function(ev) {

View file

@ -2,7 +2,7 @@
<link rel="import" href="../cards/state-card.html">
<polymer-element name="state-cards" attributes="api filter">
<polymer-element name="state-cards" attributes="states">
<template>
<style>
:host {
@ -47,7 +47,7 @@
<div horizontal layout wrap>
<template repeat="{{states as state}}">
<state-card class="state-card" stateObj={{state}} api={{api}}></state-card>
<state-card class="state-card" stateObj={{state}}></state-card>
</template>
<template if="{{states.length == 0}}">
@ -60,24 +60,6 @@
</template>
<script>
Polymer({
filter: null,
states: [],
observe: {
'api.states': 'filterChanged'
},
filterChanged: function() {
if(this.filter === 'customgroup') {
this.states = this.api.getCustomGroups();
} else {
// if no filter, return all non-group states
this.states = this.api.states.filter(function(state) {
return state.domain != 'group';
});
}
},
});
</script>
</polymer-element>

View file

@ -39,10 +39,6 @@
},
fetchData: function() {
if (!this.api) {
return;
}
this.stateData = null;
var url = 'history/period';
@ -51,11 +47,10 @@
url += '?filter_entity_id=' + this.stateObj.entity_id;
}
this.api.call_api('GET', url, {}, function(stateData) {
window.hass.callApi('GET', url).then(function(stateData) {
this.stateData = stateData;
this.drawChart();
}.bind(this)
);
}.bind(this));
},
drawChart: function() {
@ -75,7 +70,7 @@
var stateTimeToDate = function(time) {
if (!time) return new Date();
return ha.util.parseTime(time).toDate();
return window.hass.util.parseTime(time).toDate();
};
var addRow = function(baseState, state, tillState) {
@ -89,7 +84,7 @@
// this.stateData is a list of lists of sorted state objects
this.stateData.forEach(function(stateInfo) {
var baseState = new this.api.State(stateInfo[0], this.api);
var baseState = new window.hass.stateModel(stateInfo[0]);
var prevRow = null;

View file

@ -65,7 +65,7 @@ Polymer({
},
setEventData: function(eventData) {
this.$.inputData.value = eventData;
this.$.inputData.value = eventData || "";
// this.$.inputDataWrapper.update();
},
@ -75,7 +75,7 @@ Polymer({
clickFireEvent: function() {
try {
this.api.fire_event(
window.hass.eventActions.fire(
this.$.inputType.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
this.$.dialog.close();

View file

@ -16,10 +16,10 @@
</style>
<div>
<state-card-content stateObj="{{stateObj}}" api="{{api}}" class='title-card'>
<state-card-content stateObj="{{stateObj}}" class='title-card'>
</state-card-content>
<state-timeline stateObj="{{stateObj}}" api="{{api}}"></state-timeline>
<more-info-content stateObj="{{stateObj}}" api="{{api}}"></more-info-content>
<state-timeline stateObj="{{stateObj}}"></state-timeline>
<more-info-content stateObj="{{stateObj}}"></more-info-content>
</div>
<paper-button dismissive on-click={{editClicked}}>Debug</paper-button>
@ -56,7 +56,7 @@ Polymer({
},
editClicked: function(ev) {
this.api.showEditStateDialog(this.stateObj.entity_id);
window.hass.uiActions.showSetStateDialog(this.stateObj.entity_id);
}
});

View file

@ -53,7 +53,7 @@ Polymer({
show: function(domain, service, serviceData) {
this.setService(domain, service);
this.$.inputData.value = serviceData;
this.$.inputData.value = serviceData || "";
// this.$.inputDataWrapper.update();
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
@ -71,10 +71,10 @@ Polymer({
clickCallService: function() {
try {
this.api.call_service(
this.$.inputDomain.value,
this.$.inputService.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
window.hass.serviceActions.callService(
this.$.inputDomain.value,
this.$.inputService.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
this.$.dialog.close();

View file

@ -82,14 +82,14 @@ Polymer({
entitySelected: function(entityId) {
this.setEntityId(entityId);
var state = this.api.getState(entityId);
var state = window.hass.stateStore.get(entityId);
this.setState(state.state);
this.setStateData(state.attributes);
},
clickSetState: function(ev) {
try {
this.api.set_state(
window.hass.stateActions.set(
this.$.inputEntityID.value,
this.$.inputState.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}

View file

@ -1,508 +1,130 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="./bower_components/paper-toast/paper-toast.html">
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-toast/paper-toast.html">
<link rel="import" href="./dialogs/event-fire-dialog.html">
<link rel="import" href="./dialogs/service-call-dialog.html">
<link rel="import" href="./dialogs/state-set-dialog.html">
<link rel="import" href="./dialogs/more-info-dialog.html">
<link rel="import" href="./dialogs/history-dialog.html">
<link rel="import" href="dialogs/event-fire-dialog.html">
<link rel="import" href="dialogs/service-call-dialog.html">
<link rel="import" href="dialogs/state-set-dialog.html">
<link rel="import" href="dialogs/more-info-dialog.html">
<link rel="import" href="dialogs/history-dialog.html">
<script src="./home-assistant-js/dist/homeassistant.min.js"></script>
<script>
var ha = {};
ha.util = {};
ha.util.parseTime = function(timeString) {
return moment(timeString, "HH:mm:ss DD-MM-YYYY");
};
ha.util.relativeTime = function(timeString) {
return ha.util.parseTime(timeString).fromNow();
};
// Register some polymer filters
PolymerExpressions.prototype.relativeHATime = function(timeString) {
return ha.util.relativeTime(timeString);
return window.hass.util.relativeTime(timeString);
};
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
return (timeString || "").split(' ')[0];
};
</script>
<polymer-element name="home-assistant-api" attributes="auth">
<template>
<paper-toast id="toast" role="alert" text=""></paper-toast>
<event-fire-dialog id="eventDialog" api={{api}}></event-fire-dialog>
<service-call-dialog id="serviceDialog" api={{api}}></service-call-dialog>
<state-set-dialog id="stateSetDialog" api={{api}}></state-set-dialog>
<more-info-dialog id="moreInfoDialog" api={{api}}></more-info-dialog>
<history-dialog id="historyDialog" api={{api}}></history-dialog>
<event-fire-dialog id="eventDialog"></event-fire-dialog>
<service-call-dialog id="serviceDialog"></service-call-dialog>
<state-set-dialog id="stateSetDialog"></state-set-dialog>
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
<history-dialog id="historyDialog"></history-dialog>
</template>
<script>
var domainsWithCard = ['thermostat', 'configurator'];
var domainsWithMoreInfo = ['light', 'group', 'sun', 'configurator'];
State = function(json, api) {
this.api = api;
this.attributes = json.attributes;
this.entity_id = json.entity_id;
var parts = json.entity_id.split(".");
this.domain = parts[0];
this.object_id = parts[1];
if(this.attributes.friendly_name) {
this.entityDisplay = this.attributes.friendly_name;
} else {
this.entityDisplay = this.object_id.replace(/_/g, " ");
}
this.state = json.state;
this.last_changed = json.last_changed;
};
Object.defineProperties(State.prototype, {
stateDisplay: {
get: function() {
var state = this.state.replace(/_/g, " ");
if(this.attributes.unit_of_measurement) {
return state + " " + this.attributes.unit_of_measurement;
} else {
return state;
}
}
},
isCustomGroup: {
get: function() {
return this.domain == "group" && !this.attributes.auto;
}
},
canToggle: {
get: function() {
// groups that have the on/off state or if there is a turn_on service
return ((this.domain == 'group' &&
(this.state == 'on' || this.state == 'off')) ||
this.api.hasService(this.domain, 'turn_on'));
}
},
// how to render the card for this state
cardType: {
get: function() {
if(domainsWithCard.indexOf(this.domain) !== -1) {
return this.domain;
} else if(this.canToggle) {
return "toggle";
} else {
return "display";
}
}
},
// how to render the more info of this state
moreInfoType: {
get: function() {
if(domainsWithMoreInfo.indexOf(this.domain) !== -1) {
return this.domain;
} else {
return 'default';
}
}
},
relativeLastChanged: {
get: function() {
return ha.util.relativeTime(this.last_changed);
}
},
});
Polymer({
auth: "not-set",
states: [],
services: [],
events: [],
stateUpdateTimeout: null,
ready: function() {
var state,
actions = window.hass.actions,
dispatcher = window.hass.dispatcher;
// available classes
State: State,
var uiActions = window.hass.uiActions = {
ACTION_SHOW_TOAST: actions.ACTION_SHOW_TOAST,
ACTION_SHOW_DIALOG_CALL_SERVICE: 'ACTION_SHOW_DIALOG_CALL_SERVICE',
ACTION_SHOW_DIALOG_FIRE_EVENT: 'ACTION_SHOW_DIALOG_FIRE_EVENT',
ACTION_SHOW_DIALOG_SET_STATE: 'ACTION_SHOW_DIALOG_SET_STATE',
ACTION_SHOW_DIALOG_HISTORY: 'ACTION_SHOW_DIALOG_HISTORY',
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
// Polymer lifecycle methods
created: function() {
this.api = this;
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
// so we can pass these methods safely as callbacks
this.turn_on = this.turn_on.bind(this);
this.turn_off = this.turn_off.bind(this);
},
showSetStateDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_SET_STATE,
entityId: entityId
});
},
// local methods
removeState: function(entityId) {
var state = this.getState(entityId);
showFireEventDialog: function() {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_FIRE_EVENT,
});
},
if (state !== null) {
this.states.splice(this.states.indexOf(state), 1);
}
},
showCallServiceDialog: function() {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_CALL_SERVICE,
});
},
getState: function(entityId) {
var found = this.states.filter(function(state) {
return state.entity_id == entityId;
}, this);
showHistoryDialog: function() {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_HISTORY,
});
},
return found.length > 0 ? found[0] : null;
},
};
getStates: function(entityIds) {
var states = [];
var state;
for(var i = 0; i < entityIds.length; i++) {
state = this.getState(entityIds[i]);
var getState = function(entityId) {
return window.hass.stateStore.get(entityId);
};
dispatcher.register(function(payload) {
switch (payload.actionType) {
case actions.ACTION_SHOW_TOAST:
this.$.toast.text = payload.message;
this.$.toast.show();
break;
case uiActions.ACTION_SHOW_DIALOG_HISTORY:
this.$.historyDialog.show();
break;
case uiActions.ACTION_SHOW_DIALOG_MORE_INFO:
state = getState(payload.entityId);
this.$.moreInfoDialog.show(state);
break;
case uiActions.ACTION_SHOW_DIALOG_SET_STATE:
if (payload.entityId) {
state = getState(payload.entityId);
this.$.stateSetDialog.show(
state.entity_id, state.state, state.attributes);
} else {
this.$.stateSetDialog.show("", "");
}
break;
case uiActions.ACTION_SHOW_DIALOG_FIRE_EVENT:
this.$.eventDialog.show();
break;
case uiActions.ACTION_SHOW_DIALOG_CALL_SERVICE:
this.$.serviceDialog.show();
break;
if(state !== null) {
states.push(state);
}
}
return states;
},
getEntityIDs: function() {
return this.states.map(
function(state) { return state.entity_id; });
},
hasService: function(domain, service) {
var found = this.services.filter(function(serv) {
return serv.domain == domain && serv.services.indexOf(service) !== -1;
}, this);
return found.length > 0;
},
hasComponent: function(component) {
return this.components.indexOf(component) !== -1;
},
getCustomGroups: function() {
return this.states.filter(function(state) { return state.isCustomGroup;});
},
_laterFetchStates: function() {
if(this.stateUpdateTimeout) {
clearTimeout(this.stateUpdateTimeout);
}
// update states in 60 seconds
this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
},
_sortStates: function() {
this.states.sort(function(one, two) {
if (one.entity_id > two.entity_id) {
return 1;
} else if (one.entity_id < two.entity_id) {
return -1;
} else {
return 0;
}
});
},
/**
* Pushes a new state to the state machine.
* Will resort the states after a push and fire states-updated event.
*/
_pushNewState: function(new_state) {
if (this.__pushNewState(new_state)) {
this._sortStates();
}
this.fire('states-updated');
},
/**
* Creates or updates a state. Returns if a new state was added.
*/
__pushNewState: function(new_state) {
var curState = this.getState(new_state.entity_id);
if (curState === null) {
this.states.push(new State(new_state, this));
return true;
} else {
curState.attributes = new_state.attributes;
curState.last_changed = new_state.last_changed;
curState.state = new_state.state;
return false;
}
},
_pushNewStates: function(newStates, removeNonPresent) {
removeNonPresent = !!removeNonPresent;
var currentEntityIds = removeNonPresent ? this.getEntityIDs() : [];
var hasNew = newStates.reduce(function(hasNew, newState) {
var isNewState = this.__pushNewState(newState);
if (isNewState) {
return true;
} else if(removeNonPresent) {
currentEntityIds.splice(currentEntityIds.indexOf(newState.entity_id), 1);
}
return hasNew;
}.bind(this), false);
currentEntityIds.forEach(function(entityId) {
this.removeState(entityId);
}.bind(this));
if (hasNew) {
this._sortStates();
}
this.fire('states-updated');
},
// call api methods
fetchAll: function() {
this.fetchStates();
this.fetchServices();
this.fetchEvents();
this.fetchComponents();
},
fetchState: function(entityId) {
var successStateUpdate = function(new_state) {
this._pushNewState(new_state);
};
this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this));
},
fetchStates: function(onSuccess, onError) {
var successStatesUpdate = function(newStates) {
this._pushNewStates(newStates, true);
this._laterFetchStates();
if(onSuccess) {
onSuccess(this.states);
}
};
this.call_api(
"GET", "states", null, successStatesUpdate.bind(this), onError);
},
fetchEvents: function(onSuccess, onError) {
var successEventsUpdated = function(events) {
this.events = events;
this.fire('events-updated');
if(onSuccess) {
onSuccess(events);
}
};
this.call_api(
"GET", "events", null, successEventsUpdated.bind(this), onError);
},
fetchServices: function(onSuccess, onError) {
var successServicesUpdated = function(services) {
this.services = services;
this.fire('services-updated');
if(onSuccess) {
onSuccess(this.services);
}
};
this.call_api(
"GET", "services", null, successServicesUpdated.bind(this), onError);
},
fetchComponents: function(onSuccess, onError) {
var successComponentsUpdated = function(components) {
this.components = components;
this.fire('components-updated');
if(onSuccess) {
onSuccess(this.components);
}
};
this.call_api(
"GET", "components", null,
successComponentsUpdated.bind(this), onError);
},
turn_on: function(entity_id, options) {
this.call_service(
"homeassistant", "turn_on", {entity_id: entity_id}, options);
},
turn_off: function(entity_id, options) {
this.call_service(
"homeassistant", "turn_off", {entity_id: entity_id}, options);
},
set_state: function(entity_id, state, attributes) {
var payload = {state: state};
if(attributes) {
payload.attributes = attributes;
}
var successToast = function(new_state) {
this.showToast("State of "+entity_id+" set to "+state+".");
this._pushNewState(new_state);
};
this.call_api("POST", "states/" + entity_id,
payload, successToast.bind(this));
},
call_service: function(domain, service, parameters, options) {
parameters = parameters || {};
options = options || {};
var successHandler = function(changed_states) {
if(service == "turn_on" && parameters.entity_id) {
this.showToast("Turned on " + parameters.entity_id + '.');
} else if(service == "turn_off" && parameters.entity_id) {
this.showToast("Turned off " + parameters.entity_id + '.');
} else {
this.showToast("Service "+domain+"/"+service+" called.");
}
this._pushNewStates(changed_states);
if(options.success) {
options.success();
}
};
var errorHandler = function(error_data) {
if(options.error) {
options.error(error_data);
}
};
this.call_api("POST", "services/" + domain + "/" + service,
parameters, successHandler.bind(this), errorHandler);
},
fire_event: function(eventType, eventData) {
eventData = eventData || {};
var successToast = function() {
this.showToast("Event "+eventType+" fired.");
};
this.call_api("POST", "events/" + eventType,
eventData, successToast.bind(this));
},
call_api: function(method, path, parameters, onSuccess, onError) {
var url = "/api/" + path;
// set to true to generate a frontend to be used as demo on the website
if (false) {
if (path === "states" || path === "services" || path === "events") {
url = "/demo/" + path + ".json";
} else {
return;
}
}
var req = new XMLHttpRequest();
req.open(method, url, true);
req.setRequestHeader("X-HA-access", this.auth);
req.onreadystatechange = function() {
if(req.readyState == 4) {
if(req.status > 199 && req.status < 300) {
if(onSuccess) {
onSuccess(JSON.parse(req.responseText));
}
} else {
if(onError) {
var data = req.responseText ? JSON.parse(req.responseText) : {};
onError(data);
}
}
}
}.bind(this);
if(parameters) {
req.send(JSON.stringify(parameters));
} else {
req.send();
// if auth was given, tell the backend
if(this.auth) {
window.hass.authActions.validate(this.auth);
}
},
// show dialogs
showHistoryDialog: function() {
this.$.historyDialog.show();
},
showmoreInfoDialog: function(entityId) {
this.$.moreInfoDialog.show(this.getState(entityId));
},
showEditStateDialog: function(entityId) {
var state = this.getState(entityId);
this.showSetStateDialog(entityId, state.state, state.attributes);
},
showSetStateDialog: function(entityId, state, stateAttributes) {
entityId = entityId || "";
state = state || "";
stateAttributes = stateAttributes || null;
this.$.stateSetDialog.show(entityId, state, stateAttributes);
},
showFireEventDialog: function(eventType, eventData) {
eventType = eventType || "";
eventData = eventData || "";
this.$.eventDialog.show(eventType, eventData);
},
showCallServiceDialog: function(domain, service, serviceData) {
domain = domain || "";
service = service || "";
serviceData = serviceData || "";
this.$.serviceDialog.show(domain, service, serviceData);
},
showToast: function(message) {
this.$.toast.text = message;
this.$.toast.show();
},
logOut: function() {
this.auth = "";
}
});
</script>
</polymer-element>

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

View file

@ -0,0 +1,48 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/font-roboto/roboto.html">
<link rel="import" href="home-assistant-api.html">
<link rel="import" href="layouts/login-form.html">
<link rel="import" href="layouts/home-assistant-main.html">
<link rel="import" href="resources/home-assistant-style.html">
<polymer-element name="home-assistant" attributes="auth">
<template>
<style>
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}
</style>
<home-assistant-style></home-assistant-style>
<home-assistant-api auth="{{auth}}"></home-assistant-api>
<template if="{{!loaded}}">
<login-form></login-form>
</template>
<template if="{{loaded}}">
<home-assistant-main></home-assistant-main>
</template>
</template>
<script>
Polymer({
loaded: window.hass.syncStore.initialLoadDone(),
ready: function() {
// remove the HTML init message
document.getElementById('init').remove();
// listen if we are fully loaded
window.hass.syncStore.addChangeListener(this.updateLoadStatus.bind(this));
},
updateLoadStatus: function() {
this.loaded = window.hass.syncStore.initialLoadDone();
},
});
</script>
</polymer-element>

View file

@ -1,15 +1,15 @@
<link rel="import" href="bower_components/core-header-panel/core-header-panel.html">
<link rel="import" href="bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="bower_components/core-icon/core-icon.html">
<link rel="import" href="bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="bower_components/paper-tabs/paper-tab.html">
<link rel="import" href="bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="bower_components/paper-menu-button/paper-menu-button.html">
<link rel="import" href="bower_components/paper-dropdown/paper-dropdown.html">
<link rel="import" href="bower_components/core-menu/core-menu.html">
<link rel="import" href="bower_components/paper-item/paper-item.html">
<link rel="import" href="../bower_components/core-header-panel/core-header-panel.html">
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="../bower_components/core-icon/core-icon.html">
<link rel="import" href="../bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="../bower_components/paper-tabs/paper-tab.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-menu-button/paper-menu-button.html">
<link rel="import" href="../bower_components/paper-dropdown/paper-dropdown.html">
<link rel="import" href="../bower_components/core-menu/core-menu.html">
<link rel="import" href="../bower_components/paper-item/paper-item.html">
<link rel="import" href="components/state-cards.html">
<link rel="import" href="../layouts/partial-states.html">
<polymer-element name="home-assistant-main" attributes="api">
<template>
@ -69,7 +69,7 @@
</style>
<core-header-panel fit mode="{{hasCustomGroups && 'waterfall-tall'}}">
<core-header-panel fit>
<core-toolbar>
<div flex>Home Assistant</div>
@ -107,84 +107,39 @@
</core-menu>
</paper-dropdown>
</paper-menu-button>
<template if="{{hasCustomGroups}}">
<div class="bottom fit" horizontal layout>
<paper-tabs id="tabsHolder" noink flex
selected="0" on-core-select="{{tabClicked}}">
<paper-tab>ALL</paper-tab>
<paper-tab data-filter='customgroup'>GROUPS</paper-tab>
</paper-tabs>
</div>
</template>
</core-toolbar>
<state-cards
api="{{api}}"
filter="{{selectedFilter}}"
class="content">
<h3>Hi there!</h3>
<p>
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
</p>
<p>
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
</p>
</state-cards>
<partial-states></partial-states>
</core-header-panel>
</template>
<script>
Polymer({
selectedFilter: null,
hasCustomGroups: false,
observe: {
'api.states': 'updateHasCustomGroup'
},
// computed: {
// hasCustomGroups: "api.getCustomGroups().length > 0"
// },
tabClicked: function(ev) {
if(ev.detail.isSelected) {
// will be null for ALL tab
this.selectedFilter = ev.detail.item.getAttribute('data-filter');
}
},
handleRefreshClick: function() {
this.api.fetchAll();
window.hass.syncActions.sync();
},
handleHistoryClick: function() {
this.api.showHistoryDialog();
window.hass.uiActions.showHistoryDialog();
},
handleEventClick: function() {
this.api.showFireEventDialog();
window.hass.uiActions.showFireEventDialog();
},
handleServiceClick: function() {
this.api.showCallServiceDialog();
window.hass.uiActions.showCallServiceDialog();
},
handleAddStateClick: function() {
this.api.showSetStateDialog();
window.hass.uiActions.showSetStateDialog();
},
handleLogOutClick: function() {
this.api.logOut();
window.hass.authActions.logOut();
},
updateHasCustomGroup: function() {
this.hasCustomGroups = this.api.getCustomGroups().length > 0;
}
});
</script>
</polymer-element>

View file

@ -0,0 +1,129 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="../bower_components/core-input/core-input.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<polymer-element name="login-form">
<template>
<style>
paper-input {
display: block;
}
.login paper-button {
margin-left: 242px;
}
.login .interact {
height: 125px;
}
#validatebox {
text-align: center;
}
.validatemessage {
margin-top: 10px;
}
</style>
<div layout horizontal center fit class='login' id="splash">
<div layout vertical center flex>
<img src="/static/favicon-192x192.png" />
<h1>Home Assistant</h1>
<a href="#" id="hideKeyboardOnFocus"></a>
<div class='interact' layout vertical>
<div id='loginform' hidden?="{{isValidating || isLoggedIn}}">
<paper-input-decorator label="Password" id="passwordDecorator">
<input is="core-input" type="password" id="passwordInput"
value="{{authToken}}" on-keyup="{{passwordKeyup}}">
</paper-input-decorator>
<paper-button on-click={{validatePassword}}>Log In</paper-button>
</div>
<div id="validatebox" hidden?="{{!(isValidating || isLoggedIn)}}">
<paper-spinner active="true"></paper-spinner><br />
<div class="validatemessage">{{spinnerMessage}}</div>
</div>
</div>
</div>
</div>
</template>
<script>
Polymer({
MSG_VALIDATING: "Validating password…",
MSG_LOADING_DATA: "Loading data…",
authToken: "",
isValidating: false,
isLoggedIn: false,
spinnerMessage: "",
ready: function() {
this.syncStore = window.hass.syncStore;
this.authStore = window.hass.authStore;
this.authChangeListener = this.authChangeListener.bind(this);
this.authStore.addChangeListener(this.authChangeListener);
this.authChangeListener();
},
attached: function() {
this.focusPassword();
},
detached: function() {
this.authStore.removeChangeListener(this.authChangeListener);
},
authChangeListener: function() {
this.isValidating = this.authStore.isValidating();
this.isLoggedIn = this.authStore.isLoggedIn();
this.spinnerMessage = this.isValidating ? this.MSG_VALIDATING : this.MSG_LOADING_DATA;
if (this.authStore.wasLastAttemptInvalid()) {
this.$.passwordDecorator.error = this.authStore.getLastAttemptMessage();
this.$.passwordDecorator.isInvalid = true;
}
if (!(this.isValidating && this.isLoggedIn)) {
this.job('focusPasswordBox', this.focusPassword.bind(this));
}
},
focusPassword: function() {
this.$.passwordInput.focus();
},
passwordKeyup: function(ev) {
// validate on enter
if(ev.keyCode === 13) {
this.validatePassword();
// clear error after we start typing again
} else if(this.$.passwordDecorator.isInvalid) {
this.$.passwordDecorator.isInvalid = false;
}
},
validatePassword: function() {
this.$.hideKeyboardOnFocus.focus();
window.hass.authActions.validate(this.authToken);
},
});
</script>
</polymer-element>

View file

@ -0,0 +1,38 @@
<link rel="import" href="../components/state-cards.html">
<polymer-element name="partial-states">
<template>
<state-cards states="{{states}}">
<h3>Hi there!</h3>
<p>
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
</p>
<p>
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
</p>
</state-cards>
</template>
<script>
Polymer({
states: [],
ready: function() {
this.stateStoreChanged = this.stateStoreChanged.bind(this);
window.hass.stateStore.addChangeListener(this.stateStoreChanged);
this.stateStoreChanged();
},
detached: function() {
window.hass.stateStore.removeChangeListener(this.stateStoreChanged);
},
stateStoreChanged: function() {
this.states = _.filter(window.hass.stateStore.all(), function(state) {
return state.domain !== 'group';
});
},
});
</script>
</polymer>

View file

@ -76,15 +76,16 @@
configure_id: this.stateObj.attributes.configure_id
};
this.api.call_service('configurator', 'configure', data, {
success: function() {
window.hass.serviceActions.callService('configurator', 'configure', data).then(
function() {
this.action = 'display';
this.api.fetchAll();
window.hass.syncActions.sync();
}.bind(this),
error: function() {
function() {
this.action = 'display';
}.bind(this)
});
}.bind(this));
}
});
</script>

View file

@ -6,7 +6,7 @@
<link rel="import" href="more-info-sun.html">
<link rel="import" href="more-info-configurator.html">
<polymer-element name="more-info-content" attributes="api stateObj">
<polymer-element name="more-info-content" attributes="stateObj">
<template>
<style>
:host {
@ -30,7 +30,6 @@ Polymer({
}
var moreInfo = document.createElement("more-info-" + this.stateObj.moreInfoType);
moreInfo.api = this.api;
moreInfo.stateObj = this.stateObj;
this.$.moreInfo.appendChild(moreInfo);
},

View file

@ -2,7 +2,7 @@
<link rel="import" href="../cards/state-card-content.html">
<polymer-element name="more-info-group" attributes="stateObj api">
<polymer-element name="more-info-group" attributes="stateObj">
<template>
<style>
.child-card {
@ -15,7 +15,7 @@
</style>
<template repeat="{{states as state}}">
<state-card-content stateObj="{{state}}" api="{{api}}" class='child-card'>
<state-card-content stateObj="{{state}}" class='child-card'>
</state-card-content>
</template>
</template>
@ -26,7 +26,7 @@ Polymer({
},
updateStates: function() {
this.states = this.api.getStates(this.stateObj.attributes.entity_id);
this.states = window.hass.stateStore.gets(this.stateObj.attributes.entity_id);
}
});
</script>

View file

@ -56,9 +56,8 @@
<script>
Polymer({
// on-change is unpredictable so using on-core-change this has side effect
// that it fires if changed by brightnessChanged(), thus an ignore boolean.
ignoreNextBrightnessEvent: false,
// initial change should be ignored
ignoreNextBrightnessEvent: true,
observe: {
'stateObj.attributes.brightness': 'brightnessChanged',
@ -86,9 +85,9 @@ Polymer({
if(isNaN(bri)) return;
if(bri === 0) {
this.api.turn_off(this.stateObj.entity_id);
window.hass.serviceActions.callTurnOff(this.stateObj.entity_id);
} else {
this.api.call_service("light", "turn_on", {
window.hass.serviceActions.callService("light", "turn_on", {
entity_id: this.stateObj.entity_id,
brightness: bri
});
@ -98,7 +97,7 @@ Polymer({
colorPicked: function(ev) {
var color = ev.detail.rgb;
this.api.call_service("light", "turn_on", {
window.hass.serviceActions.callService("light", "turn_on", {
entity_id: this.stateObj.entity_id,
rgb_color: [color.r, color.g, color.b]
});

View file

@ -31,8 +31,8 @@
Polymer({
stateObjChanged: function() {
var rising = ha.util.parseTime(this.stateObj.attributes.next_rising);
var setting = ha.util.parseTime(this.stateObj.attributes.next_setting);
var rising = window.hass.parseTime(this.stateObj.attributes.next_rising);
var setting = window.hass.parseTime(this.stateObj.attributes.next_setting);
if(rising > setting) {
this.$.sunData.appendChild(this.$.rising);

View file

@ -1,157 +0,0 @@
<link rel="import" href="bower_components/font-roboto/roboto.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
<link rel="import" href="bower_components/core-input/core-input.html">
<link rel="import" href="bower_components/paper-spinner/paper-spinner.html">
<link rel="import" href="home-assistant-main.html">
<link rel="import" href="home-assistant-api.html">
<link rel="import" href="resources/home-assistant-style.html">
<polymer-element name="splash-login" attributes="auth">
<template>
<style>
:host {
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}
paper-input {
display: block;
}
.login paper-button {
margin-left: 242px;
}
.login .interact {
height: 125px;
}
#validatebox {
text-align: center;
}
#validatemessage {
margin-top: 10px;
}
</style>
<home-assistant-style></home-assistant-style>
<home-assistant-api auth="{{auth}}" id="api"></home-assistant-api>
<div layout horizontal center fit class='login' id="splash">
<div layout vertical center flex>
<img src="/static/favicon-192x192.png" />
<h1>Home Assistant</h1>
<a href="#" id="hideKeyboardOnFocus"></a>
<div class='interact' layout vertical>
<div id='loginform'>
<paper-input-decorator label="Password" id="passwordDecorator">
<input is="core-input" type="password" id="passwordInput"
value="{{auth}}" on-keyup="{{passwordKeyup}}" autofocus>
</paper-input-decorator>
<paper-button on-click={{validatePassword}}>Log In</paper-button>
</div>
<div id="validatebox" hidden>
<paper-spinner active="true"></paper-spinner><br />
<div id="validatemessage">Validating password...</div>
</div>
</div>
</div>
</div>
<home-assistant-main api="{{api}}" hidden id="main"></home-assistant-main>
</template>
<script>
Polymer({
// can be no_auth, valid_auth
state: "no_auth",
auth: "",
ready: function() {
this.api = this.$.api;
},
domReady: function() {
document.getElementById('init').remove();
if(this.auth) {
this.validatePassword();
}
},
authChanged: function(oldVal, newVal) {
// log out functionality
if(newVal === "" && this.state === "valid_auth") {
this.state = "no_auth";
}
},
stateChanged: function(oldVal, newVal) {
if(newVal === "no_auth") {
// set login box showing
this.$.loginform.removeAttribute('hidden');
this.$.validatebox.setAttribute('hidden', null);
// reset to initial message
this.$.validatemessage.innerHTML = "Validating password...";
// show splash
this.$.splash.removeAttribute('hidden');
this.$.main.setAttribute('hidden', null);
} else { // valid_auth
this.$.splash.setAttribute('hidden', null);
this.$.main.removeAttribute('hidden');
}
},
passwordKeyup: function(ev) {
// validate on enter
if(ev.keyCode === 13) {
this.validatePassword();
// clear error after we start typing again
} else if(this.$.passwordDecorator.isInvalid) {
this.$.passwordDecorator.isInvalid = false;
}
},
validatePassword: function() {
this.$.loginform.setAttribute('hidden', null);
this.$.validatebox.removeAttribute('hidden');
this.$.hideKeyboardOnFocus.focus();
var passwordValid = function(result) {
this.$.validatemessage.innerHTML = "Loading data…";
this.api.fetchEvents();
this.api.fetchComponents();
this.api.fetchStates(function() {
this.state = "valid_auth";
}.bind(this));
};
var passwordInvalid = function(result) {
if(result && result.message) {
this.$.passwordDecorator.error = result.message;
} else {
this.$.passwordDecorator.error = "Unexpected result from API";
}
this.auth = null;
this.$.passwordDecorator.isInvalid = true;
this.$.loginform.removeAttribute('hidden');
this.$.validatebox.setAttribute('hidden', null);
this.$.passwordInput.focus();
};
this.api.fetchServices(passwordValid.bind(this), passwordInvalid.bind(this));
}
});
</script>
</polymer-element>

View file

@ -15,7 +15,7 @@ cp polymer/bower_components/webcomponentsjs/webcomponents.min.js .
# Let Polymer refer to the minified JS version before we compile
sed -i.bak 's/polymer\.js/polymer\.min\.js/' polymer/bower_components/polymer/polymer.html
vulcanize -o frontend.html --inline --strip polymer/splash-login.html
vulcanize -o frontend.html --inline --strip polymer/home-assistant.html
# Revert back the change to the Polymer component
rm polymer/bower_components/polymer/polymer.html

9
scripts/build_js Executable file
View file

@ -0,0 +1,9 @@
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
cd homeassistant/components/frontend/www_static/polymer/home-assistant-js
npm install
npm run prod

9
scripts/dev_js Executable file
View file

@ -0,0 +1,9 @@
# If current pwd is scripts, go 1 up.
if [ ${PWD##*/} == "scripts" ]; then
cd ..
fi
cd homeassistant/components/frontend/www_static/polymer/home-assistant-js
npm install
npm run dev