Frontend streams events from HA

This commit is contained in:
Paulus Schoutsen 2015-02-13 18:59:42 -08:00
parent 3f26fc3b06
commit 846e11d6b8
14 changed files with 228 additions and 91 deletions

View file

@ -6,6 +6,8 @@ Provides a Rest API for Home Assistant.
"""
import re
import logging
import threading
import json
import homeassistant as ha
from homeassistant.helpers import TrackStates
@ -34,6 +36,9 @@ def setup(hass, config):
# /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api)
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
@ -79,6 +84,40 @@ def _handle_get_api(handler, path_match, data):
handler.write_json_message("API running.")
def _handle_get_api_stream(handler, path_match, data):
""" Provide a streaming interface for the event bus. """
hass = handler.server.hass
wfile = handler.wfile
block = threading.Event()
def event_sourcer(event):
""" Forwards events to the open request. """
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
return
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
block.set()
return
msg = "data: {}\n\n".format(
json.dumps(event.as_dict(), cls=rem.JSONEncoder))
try:
wfile.write(msg.encode("UTF-8"))
wfile.flush()
except IOError:
block.set()
handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/event-stream')
handler.end_headers()
hass.bus.listen(MATCH_ALL, event_sourcer)
block.wait()
hass.bus.remove_listener(MATCH_ALL, event_sourcer)
def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """
handler.write_json(handler.server.hass.states.all())

View file

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "873a4efef163787ea768e761f47354a2"
VERSION = "3c4dc2ed787b1b4c50b17f4b2bedf0c4"

File diff suppressed because one or more lines are too long

View file

@ -1,23 +1,12 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-toggle" attributes="stateObj api">
<template>
<style>
paper-toggle-button::shadow .toggle-ink {
color: #039be5;
}
paper-toggle-button::shadow [checked] .toggle-bar {
background-color: #039be5;
}
paper-toggle-button::shadow [checked] .toggle-button {
background-color: #039be5;
}
</style>
<core-style ref='ha-paper-toggle'></core-style>
<div horizontal justified layout>
<state-info flex stateObj="{{stateObj}}"></state-info>

View file

@ -11,10 +11,6 @@
}
@media all and (min-width: 1020px) {
:host {
/*padding-bottom: 8px;*/
}
.state-card {
width: calc(50% - 44px);
margin: 8px 0 0 8px;
@ -24,7 +20,6 @@
@media all and (min-width: 1356px) {
.state-card {
width: calc(33% - 38px);
}
}
@ -40,7 +35,7 @@
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
padding: 16px;
margin: 0 auto;
margin: 16px auto;
}
</style>

View file

@ -0,0 +1,60 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/core-icons/notification-icons.html">
<polymer-element name="stream-status">
<template>
<style>
:host {
display: inline-block;
height: 24px;
}
paper-toggle-button {
vertical-align: middle;
}
</style>
<core-style ref='ha-paper-toggle'></core-style>
<core-icon icon="warning" hidden?="{{!hasError}}"></core-icon>
<paper-toggle-button id="toggle" on-change={{toggleChanged}} hidden?="{{hasError}}"></paper-toggle-button>
</template>
<script>
var streamActions = window.hass.streamActions;
var authStore = window.hass.authStore;
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
isStreaming: false,
hasError: false,
icon: "swap-vert-circle",
color: 'red',
ready: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming();
this.hasError = streamStore.hasError();
this.$.toggle.checked = this.isStreaming;
},
toggleChanged: function(ev) {
if (this.isStreaming) {
streamActions.stop();
} else {
streamActions.start(authStore.getAuthToken());
}
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -27,6 +27,7 @@
Polymer({
stateObj: {},
stateHistory: null,
hasHistoryComponent: false,
observe: {
'stateObj.attributes': 'reposition'
@ -42,6 +43,10 @@ Polymer({
window.hass.stateHistoryStore.removeChangeListener(this.stateHistoryStoreChanged);
},
componentStoreChanged: function() {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
stateHistoryStoreChanged: function() {
if (this.stateObj.entityId) {
this.stateHistory = window.hass.stateHistoryStore.get(this.stateObj.entityId);
@ -53,7 +58,7 @@ Polymer({
stateObjChanged: function() {
if (this.stateObj.entityId &&
window.hass.stateHistoryStore.isStale(this.stateObj.entityId)) {
window.hass.stateHistoryActions.fetch(this.stateObj.entityId);
// window.hass.stateHistoryActions.fetch(this.stateObj.entityId);
}
this.stateHistoryStoreChanged();

View file

@ -51,8 +51,29 @@
return moment(this.lastChangedAsDate).fromNow();
}
},
});
var state,
constants = window.hass.constants,
dispatcher = window.hass.dispatcher,
preferenceStore = window.hass.preferenceStore;
var uiActions = window.hass.uiActions = {
ACTION_SHOW_TOAST: constants.ACTION_SHOW_TOAST,
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
validateAuth: function(authToken) {
window.hass.authActions.validate(authToken, preferenceStore.useStreaming());
},
};
</script>
<polymer-element name="home-assistant-api" attributes="auth">
@ -63,30 +84,11 @@
<script>
Polymer({
ready: function() {
var state,
actions = window.hass.actions,
dispatcher = window.hass.dispatcher;
var uiActions = window.hass.uiActions = {
ACTION_SHOW_TOAST: actions.ACTION_SHOW_TOAST,
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
};
var getState = function(entityId) {
return window.hass.stateStore.get(entityId);
};
var getState = window.hass.stateStore.get;
dispatcher.register(function(payload) {
switch (payload.actionType) {
case actions.ACTION_SHOW_TOAST:
case constants.ACTION_SHOW_TOAST:
this.$.toast.text = payload.message;
this.$.toast.show();
break;
@ -101,10 +103,9 @@
// if auth was given, tell the backend
if(this.auth) {
window.hass.authActions.validate(this.auth);
window.hass.uiActions.validateAuth(this.auth);
}
},
});
</script>
</polymer-element>

@ -1 +1 @@
Subproject commit b32562325cf981691a18c88776a10b688cc7c6e6
Subproject commit 7e398ff5239a2d5335a8c32bbe69f0e3c61fa07f

View file

@ -14,6 +14,8 @@
<link rel="import" href="../layouts/partial-dev-call-service.html">
<link rel="import" href="../layouts/partial-dev-set-state.html">
<link rel="import" href="../components/stream-status.html">
<polymer-element name="home-assistant-main">
<template>
<core-style ref="ha-headers"></core-style>
@ -48,12 +50,15 @@
min-height: 53px;
}
.seperator {
.text {
padding: 16px;
font-size: 14px;
border-top: 1px solid #e0e0e0;
}
.label {
font-size: 14px;
}
.dev-tools {
padding: 0 8px;
}
@ -85,12 +90,17 @@
<div flex></div>
<paper-item id="logout" on-click="{{handleLogOutClick}}">
<paper-item on-click="{{handleLogOutClick}}">
<core-icon icon="exit-to-app"></core-icon>
Log Out
</paper-item>
<div class='seperator'>Developer Tools</div>
<div class='text' horizontal layout center>
<div flex>Streaming updates</div>
<stream-status></stream-status>
</div>
<div class='text label'>Developer Tools</div>
<div class='dev-tools' layout horizontal justified>
<paper-icon-button
icon="settings-remote" data-panel='call-service'
@ -131,26 +141,37 @@
</template>
<script>
Polymer({
var componentStore = window.hass.componentStore;
var streamStore = window.hass.streamStore;
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
selected: "states",
narrow: false,
hasHistoryComponent: false,
isStreaming: false,
hasStreamError: false,
ready: function() {
this.togglePanel = this.togglePanel.bind(this);
this.componentStoreChanged = this.componentStoreChanged.bind(this);
window.hass.componentStore.addChangeListener(this.componentStoreChanged);
this.componentStoreChanged();
this.listenToStores(true);
},
detached: function() {
window.hass.componentStore.removeChangeListener(this.componentStoreChanged);
this.stopListeningToStores();
},
componentStoreChanged: function() {
this.hasHistoryComponent = window.hass.componentStore.isLoaded('history');
this.hasHistoryComponent = componentStore.isLoaded('history');
},
streamStoreChanged: function() {
var state = streamStore.getState();
this.isStreaming = state === streamStore.STATE_CONNECTED;
this.hasStreamError = state === streamStore.STATE_ERROR;
},
menuSelect: function(ev, detail, sender) {
@ -185,6 +206,6 @@ Polymer({
window.hass.authActions.logOut();
},
});
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -58,7 +58,9 @@
</template>
<script>
Polymer({
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
MSG_VALIDATING: "Validating password…",
MSG_LOADING_DATA: "Loading data…",
@ -70,13 +72,7 @@
spinnerMessage: "",
ready: function() {
this.authStore = window.hass.authStore;
this.authChangeListener = this.authChangeListener.bind(this);
this.authStore.addChangeListener(this.authChangeListener);
this.authChangeListener();
this.listenToStores(true);
},
attached: function() {
@ -84,7 +80,7 @@
},
detached: function() {
this.authStore.removeChangeListener(this.authChangeListener);
this.stopListeningToStores();
},
authChangeListener: function() {
@ -120,10 +116,10 @@
validatePassword: function() {
this.$.hideKeyboardOnFocus.focus();
window.hass.authActions.validate(this.authToken);
window.hass.uiActions.validateAuth(this.authToken);
},
});
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -1,4 +1,6 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html">

View file

@ -1,4 +1,6 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html">
@ -13,7 +15,7 @@
<span header-buttons>
<paper-icon-button icon="refresh" class="{{isFetching && 'ha-spin'}}"
on-click="{{handleRefreshClick}}"></paper-icon-button>
on-click="{{handleRefreshClick}}" hidden?="{{isStreaming}}"></paper-icon-button>
</span>
<state-cards states="{{states}}">
@ -28,24 +30,20 @@
</partial-base>
</template>
<script>
Polymer({
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
headerTitle: "States",
states: [],
isFetching: false,
isStreaming: false,
ready: function() {
this.stateStoreChanged = this.stateStoreChanged.bind(this);
window.hass.stateStore.addChangeListener(this.stateStoreChanged);
this.syncStoreChanged = this.syncStoreChanged.bind(this);
window.hass.syncStore.addChangeListener(this.syncStoreChanged);
this.refreshStates();
this.listenToStores(true);
},
detached: function() {
window.hass.stateStore.removeChangeListener(this.stateStoreChanged);
window.hass.syncStore.removeChangeListener(this.syncStoreChanged);
this.stopListeningToStores();
},
stateStoreChanged: function() {
@ -56,6 +54,10 @@
this.isFetching = window.hass.syncStore.isFetching();
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming();
},
filterChanged: function() {
this.refreshStates();
@ -85,6 +87,6 @@
handleRefreshClick: function() {
window.hass.syncActions.sync();
},
});
}, storeListenerMixIn));
</script>
</polymer>

View file

@ -111,3 +111,17 @@
word-break: break-all;
}
</core-style>
<core-style id='ha-paper-toggle'>
paper-toggle-button::shadow .toggle-ink {
color: #039be5;
}
paper-toggle-button::shadow [checked] .toggle-bar {
background-color: #039be5;
}
paper-toggle-button::shadow [checked] .toggle-button {
background-color: #039be5;
}
</core-style>