Initial commit Polymer interface

This commit is contained in:
Paulus Schoutsen 2014-10-22 00:02:18 -07:00
parent a0c12fe685
commit 8a8097af99
11 changed files with 993 additions and 0 deletions

View File

@ -0,0 +1,30 @@
{
"name": "Home Assistant",
"version": "0.1.0",
"authors": [
"Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
],
"main": "index.htm",
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"polymer": "Polymer/polymer#~0.4.2",
"font-roboto": "Polymer/font-roboto#~0.4.2",
"core-header-panel": "Polymer/core-header-panel#~0.4.2",
"core-toolbar": "Polymer/core-toolbar#~0.4.2",
"core-icon-button": "Polymer/core-icon-button#~0.4.2",
"paper-fab": "Polymer/paper-fab#~0.4.2",
"core-ajax": "Polymer/core-ajax#~0.4.2",
"paper-toast": "Polymer/paper-toast#~0.4.2",
"paper-dialog": "Polymer/paper-dialog#~0.4.2",
"paper-button": "Polymer/paper-button#~0.4.2",
"core-tooltip": "Polymer/core-tooltip#~0.4.2"
}
}

View File

@ -0,0 +1,55 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<polymer-element name="entity-list" attributes="api cbEntityClicked">
<template>
<style>
:host {
display: block;
}
.entityContainer {
font-size: 1rem;
}
</style>
<template if={{cbEntityClicked}}>
<style>
a {
text-decoration: underline;
cursor: pointer;
}
</style>
</template>
<div>
<template repeat="{{state in states}}">
<div class='eventContainer'>
<a on-click={{handleClick}}>{{state.entity_id}}</a>
</div>
</template>
</div>
</template>
<script>
Polymer({
cbEventClicked: null,
states: [],
domReady: function() {
this.api.addEventListener('states-updated', this.statesUpdated.bind(this))
this.statesUpdated()
},
statesUpdated: function() {
this.states = this.api.states;
},
handleClick: function(ev) {
if(this.cbEntityClicked) {
this.cbEntityClicked(ev.path[0].innerHTML);
}
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,71 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="events-list.html">
<polymer-element name="event-fire-dialog" attributes="api">
<template>
<style>
paper-input:first-child {
padding-top: 0;
}
.eventContainer {
margin-left: 30px;
}
</style>
<paper-dialog id="dialog" heading="Fire Event" transition="paper-dialog-transition-bottom" backdrop="true">
<div layout horizontal>
<div>
<paper-input id="inputType" label="Event Type" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputData" label="Event Data (JSON, optional)" floatingLabel="true" multiline></paper-input>
</div>
<div class='eventContainer'>
<b>Available events:</b>
<events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
</paper-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.eventSelected = this.eventSelected.bind(this)
},
show: function(eventType, eventData) {
this.setEventType(eventType);
this.setEventData(eventData);
this.$.dialog.toggle();
},
setEventType: function(eventType) {
this.$.inputType.value = eventType;
},
setEventData: function(eventData) {
this.$.inputData.value = eventData;
},
eventSelected: function(eventType) {
this.setEventType(eventType);
},
clickFireEvent: function() {
this.api.fire_event(
this.$.inputType.value,
this.$.inputData.value
)
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,58 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<polymer-element name="events-list" attributes="api cbEventClicked">
<template>
<style>
:host {
display: block;
}
.eventContainer {
font-size: 1rem;
}
</style>
<template if={{cbEventClicked}}>
<style>
a {
text-decoration: underline;
cursor: pointer;
}
</style>
</template>
<div>
<template repeat="{{event in events}}">
<div class='eventContainer'>
<a on-click={{handleClick}}>{{event.event}}</a>
({{event.listener_count}} listeners)
</div>
</template>
</div>
</template>
<script>
Polymer({
cbEventClicked: null,
events: [],
domReady: function() {
this.events = this.api.events
this.api.addEventListener('events-updated', this.eventsUpdated.bind(this))
},
eventsUpdated: function() {
this.events = this.api.events;
},
handleClick: function(ev) {
if(this.cbEventClicked) {
this.cbEventClicked(ev.path[0].innerHTML);
}
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,270 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-ajax/core-ajax.html">
<link rel="import" href="bower_components/paper-toast/paper-toast.html">
<link rel="import" href="event-fire-dialog.html">
<link rel="import" href="service-call-dialog.html">
<link rel="import" href="state-set-dialog.html">
<polymer-element name="home-assistant-api" attributes="auth">
<template>
<style>
core-ajax {
display: none;
}
</style>
<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="stateDialog" api={{api}}></state-set-dialog>
<core-ajax id="statesAjax"
auto
method="GET"
url="/api/states"
headers='{"HA-access": "{{auth}}"}'
on-core-response="{{statesLoaded}}"
handleAs="json">
</core-ajax>
<core-ajax id="eventsAjax"
auto
method="GET"
url="/api/events"
headers='{"HA-access": "{{auth}}"}'
on-core-response="{{eventsLoaded}}"
handleAs="json">
</core-ajax>
<core-ajax id="servicesAjax"
auto
method="GET"
url="/api/services"
headers='{"HA-access": "{{auth}}"}'
on-core-response="{{servicesLoaded}}"
handleAs="json">
</core-ajax>
</template>
<script>
Polymer({
auth: "",
states: [],
services: {},
events: {},
stateUpdateTimeout: null,
created: function() {
this.api = this;
// 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);
},
_laterFetchStates: function() {
if(this.stateUpdateTimeout) {
clearTimeout(this.stateUpdateTimeout);
}
// update states in 60 seconds
this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
},
_sortStates: function(states) {
return 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;
}
})
},
statesLoaded: function() {
// Make a copy of the loaded data
this.states = this._sortStates(this.$.statesAjax.response.slice(0));
this.fire('states-updated')
this._laterFetchStates();
},
eventsLoaded: function() {
// Make a copy of the loaded data
this.events = this.$.eventsAjax.response;
this.fire('events-updated')
},
servicesLoaded: function() {
// Make a copy of the loaded data
this.services = this.$.servicesAjax.response;
this.fire('services-updated')
},
_pushNewState: function(new_state) {
var state;
var stateFound = false;
for(var i = 0; i < this.states.length; i++) {
if(this.states[i].entity_id == new_state.entity_id) {
state = this.states[i];
state.attributes = new_state.attributes;
state.last_changed = new_state.last_changed;
state.state = new_state.state;
stateFound = true;
break;
}
}
if(!stateFound) {
this.states.push(new_state);
this._sortStates(this.states);
}
},
fetchState: function(entity_id) {
var successStateUpdate = function(new_state) {
this._pushNewState(new_state);
}
this.call_api("GET", "states/" + entity_id, null, successStateUpdate.bind(this));
},
fetchStates: function() {
this.$.statesAjax.go();
},
getState: function(entityId) {
for(var i = 0; i < this.states.length; i++) {
if(this.states[i].entity_id == entityId) {
return this.states[i];
}
}
},
turn_on: function(entity_id) {
this.call_service("homeassistant", "turn_on", {entity_id: entity_id});
},
turn_off: function(entity_id) {
this.call_service("homeassistant", "turn_off", {entity_id: entity_id})
},
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+" successful set to "+state+".");
this._pushNewState(new_state);
}
this.call_api("POST", "states/" + entity_id,
payload, successToast.bind(this));
},
call_service: function(domain, service, parameters) {
var successToast = function() {
this.showToast("Service "+domain+"/"+service+" successful called.");
}
this.call_api("POST", "services/" + domain + "/" + service,
parameters, successToast.bind(this));
},
fire_event: function(eventType, eventData) {
eventData = eventData ? JSON.parse(eventData) : "";
var successToast = function() {
this.showToast("Event "+eventType+" successful fired.");
}
this.call_api("POST", "events/" + eventType,
eventData, successToast.bind(this));
},
call_api: function(method, path, parameters, callback) {
var req = new XMLHttpRequest();
req.open(method, "/api/" + path, true)
req.setRequestHeader("HA-access", this.auth);
req.onreadystatechange = function() {
if(req.readyState == 4 && req.status > 199 && req.status < 300) {
if(callback) {
callback(JSON.parse(req.responseText))
}
// if we targetted an entity id, update state after 2 seconds
if(parameters && parameters.entity_id) {
var updateCallback;
// if a string, update just that entity, otherwise update all
if(typeof(parameters.entity_id) == "string") {
updateCallback = function() {
this.fetchState(parameters.entity_id);
}
} else {
updateCallback = this.fetchStates();
}
setTimeout(updateCallback.bind(this), 2000);
}
}
}.bind(this)
if(parameters) {
req.send(JSON.stringify(parameters))
} else {
req.send()
}
},
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.$.stateDialog.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();
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,79 @@
<link rel="import" href="bower_components/font-roboto/roboto.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-button/core-icon-button.html">
<link rel="import" href="bower_components/paper-fab/paper-fab.html">
<link rel="import" href="home-assistant-api.html">
<link rel="import" href="states-cards.html">
<polymer-element name="home-assistant-main" attributes="auth">
<template>
<style type="text/css">
:host {
font-family: 'RobotoDraft', sans-serif;
}
core-header-panel {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
core-toolbar {
background: #03a9f4;
font-size: 1.5rem;
color: white;
}
paper-fab {
position: absolute;
bottom: 10px;
right: 10px;
}
</style>
<home-assistant-api auth="{{auth}}" id="api"></home-assistant-api>
<core-header-panel layout>
<core-toolbar>
<div flex>
Home Assistant
</div>
<core-icon-button icon="developer-mode-tv" on-click="{{handleEventClick}}"></core-icon-button>
<core-icon-button icon="settings-remote" on-click="{{handleServiceClick}}"></core-icon-button>
</core-toolbar>
<div class="content" flex>
<states-cards api="{{api}}"></states-cards>
<paper-fab icon="add" on-click={{handleAddStateClick}}></paper-fab>
</div>
</core-header-panel>
</template>
<script>
Polymer({
ready: function() {
this.api = this.$.api;
},
handleEventClick: function() {
this.api.showFireEventDialog();
},
handleServiceClick: function() {
this.api.showCallServiceDialog();
},
handleAddStateClick: function() {
this.api.showSetStateDialog();
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,69 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="services-list.html">
<polymer-element name="service-call-dialog" attributes="api">
<template>
<style>
paper-input:first-child {
padding-top: 0;
}
.serviceContainer {
margin-left: 30px;
}
</style>
<paper-dialog id="dialog" heading="Call Service" transition="paper-dialog-transition-bottom" backdrop="true">
<div layout horizontal>
<div>
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
<paper-input id="inputData" label="Service Data (JSON, optional)" floatingLabel="true" multiline></paper-input>
</div>
<div class='serviceContainer'>
<b>Available services:</b>
<services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
</paper-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.serviceSelected = this.serviceSelected.bind(this)
},
show: function(domain, service, serviceData) {
this.setService(domain, service);
this.$.inputData.value = serviceData;
this.$.dialog.toggle();
},
setService: function(domain, service) {
this.$.inputDomain.value = domain;
this.$.inputService.value = service;
},
serviceSelected: function(domain, service) {
this.setService(domain, service);
},
clickCallService: function() {
this.api.call_service(
this.$.inputDomain.value,
this.$.inputService.value,
this.$.inputData.value
)
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,74 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-menu/core-menu.html">
<link rel="import" href="bower_components/core-menu/core-submenu.html">
<link rel="import" href="bower_components/core-item/core-item.html">
<polymer-element name="services-list" attributes="api cbServiceClicked">
<template>
<style>
:host {
display: block;
}
core-menu {
margin-top: 0;
font-size: 1rem;
}
a {
display: block;
}
</style>
<template if={{cbServiceClicked}}>
<style>
a {
text-decoration: underline;
cursor: pointer;
}
</style>
</template>
<div>
<core-menu selected="0">
<template repeat="{{serv in services}}">
<core-submenu icon="settings" label="{{serv.domain}}">
<template repeat="{{service in serv.services}}">
<a on-click={{serviceClicked}} data-domain={{serv.domain}}>{{service}}</a>
</template>
</core-submenu>
</template>
</core-menu>
</div>
</template>
<script>
Polymer({
services: [],
cbServiceClicked: null,
domReady: function() {
this.services = this.api.services
this.api.addEventListener('services-updated', this.servicesUpdated.bind(this))
},
servicesUpdated: function() {
this.services = this.api.services;
},
serviceClicked: function(ev) {
if(this.cbServiceClicked) {
var target = ev.path[0];
var domain = target.getAttributeNode("data-domain").value;
var service = target.innerHTML;
this.cbServiceClicked(domain, service);
}
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,139 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<polymer-element name="state-card"
attributes="entity state last_changed state_attr cb_turn_on, cb_turn_off cb_edit">
<template>
<style>
:host {
position: relative;
background-color: white;
padding: 20px 20px 50px 20px;
width: 100%;
font-weight: 300;
border-radius: 2px;
}
.header {
text-transform: capitalize;
font-size: 1.5rem;
}
.subheader {
margin-top: -5px;
color: darkgrey;
}
.content {
margin-top: 10px;
}
.actions {
position: absolute;
bottom: 10px;
left: 20px;
right: 20px;
text-align: right;
}
paper-button.toggle {
color: #03a9f4;
}
</style>
<div class="header" horizontal justified layout>
<span class="entity_id">{{entity_id | makeReadable}}</span>
<span class='state'>{{state | makeReadable}}</span>
</div>
<div class="subheader" horizontal justified layout>
<span class="domain">{{domain}}</span>
<core-tooltip label="{{last_changed}}" position="bottom">
<span class="last_changed_from_now">{{last_changed_from_now}}</span>
</core-tooltip>
</div>
<div class="content">
<template repeat="{{key in objectKeys(state_attr)}}">
<div>{{key | makeReadable}}: {{state_attr[key]}}</div>
</template>
</div>
<div class="actions">
<paper-button class='edit' on-click="{{editClicked}}">EDIT</paper-button>
<template if="{{state == 'on'}}">
<paper-button class="toggle" on-click="{{turn_off}}">TURN OFF</paper-button>
</template>
<template if="{{state == 'off'}}">
<paper-button class="toggle" on-click="{{turn_on}}">TURN ON</paper-button>
</template>
</div>
</template>
<script>
Polymer({
// attributes
entity: "",
state: "",
last_changed: "never",
state_attr: {},
cb_turn_on: null,
cb_turn_off: null,
cb_edit: null,
// computed
domain: "",
entity_id: "",
entityChanged: function(oldVal, newVal) {
var parts = newVal.split(".")
if(parts.length == 1) {
this.domain = ""
this.entity_id = parts[0]
} else {
this.domain = parts[0]
this.entity_id = parts.slice(1).join('.')
}
},
last_changedChanged: function(oldVal, newVal) {
this.last_changed_from_now = moment(this.last_changed, "HH:mm:ss DD-MM-YYYY").fromNow()
},
turn_on: function() {
if(this.cb_turn_on) {
this.cb_turn_on(this.entity);
}
},
turn_off: function() {
if(this.cb_turn_off) {
this.cb_turn_off(this.entity);
}
},
editClicked: function() {
if(this.cb_edit) {
this.cb_edit(this.entity);
}
},
// used as filter
makeReadable: function(value) {
return value.replace("_", " ")
},
objectKeys: function(obj) {
return obj ? Object.keys(obj) : [];
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,84 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="entity-list.html">
<polymer-element name="state-set-dialog" attributes="api">
<template>
<style>
paper-input:first-child {
padding-top: 0;
}
.stateContainer {
margin-left: 30px;
}
</style>
<paper-dialog id="dialog" heading="Set State" transition="paper-dialog-transition-center" backdrop="true">
<div layout horizontal>
<div>
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
<paper-input id="inputData" label="State attributes (JSON, optional)" floatingLabel="true" multiline></paper-input>
</div>
<div class='stateContainer'>
<b>Current entities:</b>
<entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
</div>
</div>
<paper-button dismissive>Cancel</paper-button>
<paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
</paper-dialog>
</template>
<script>
Polymer({
ready: function() {
// to ensure callback methods work..
this.entitySelected = this.entitySelected.bind(this)
},
show: function(entityId, state, stateData) {
this.setEntityId(entityId);
this.setState(state);
this.setStateData(stateData);
this.$.dialog.toggle();
},
setEntityId: function(entityId) {
this.$.inputEntityID.value = entityId;
},
setState: function(state) {
this.$.inputState.value = state;
},
setStateData: function(stateData) {
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
this.$.inputData.value = value;
},
entitySelected: function(entityId) {
this.setEntityId(entityId);
var state = this.api.getState(entityId);
this.setState(state.state);
this.setStateData(state.attributes);
},
clickSetState: function() {
this.api.set_state(
this.$.inputEntityID.value,
this.$.inputState.value,
JSON.parse(this.$.inputData.value)
)
}
});
</script>
</polymer-element>

View File

@ -0,0 +1,64 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="state-card.html">
<polymer-element name="states-cards" attributes="api">
<template>
<style>
:host {
display: block;
width: 100%;
}
state-card, state-add-card {
display: inline-block;
width: 350px;
margin: 10px 0 0 10px;
}
state-add-card {
cursor: pointer;
}
</style>
<div horizontal layout wrap>
<template repeat="{{state in states}}">
<state-card
entity="{{state.entity_id}}"
state="{{state.state}}"
last_changed="{{state.last_changed}}"
state_attr="{{state.attributes}}"
cb_turn_on="{{api.turn_on}}"
cb_turn_off="{{api.turn_off}}"
cb_edit={{editCallback}}>
</state-card>
</template>
</div>
</template>
<script>
Polymer({
states: [],
ready: function() {
this.editCallback = this.editCallback.bind(this);
},
domReady: function() {
this.states = this.api.states
this.api.addEventListener('states-updated', this.statesUpdated.bind(this))
},
statesUpdated: function() {
this.states = this.api.states;
},
editCallback: function(entityId) {
this.api.showEditStateDialog(entityId);
},
});
</script>
</polymer-element>