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 re
import logging import logging
import threading
import json
import homeassistant as ha import homeassistant as ha
from homeassistant.helpers import TrackStates from homeassistant.helpers import TrackStates
@ -34,6 +36,9 @@ def setup(hass, config):
# /api - for validation purposes # /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api) 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 # /states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states) hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path( hass.http.register_path(
@ -79,6 +84,40 @@ def _handle_get_api(handler, path_match, data):
handler.write_json_message("API running.") 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): def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """ """ Returns a dict containing all entity ids and their state. """
handler.write_json(handler.server.hass.states.all()) handler.write_json(handler.server.hass.states.all())

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ 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/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.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"> <link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-toggle" attributes="stateObj api"> <polymer-element name="state-card-toggle" attributes="stateObj api">
<template> <template>
<style> <core-style ref='ha-paper-toggle'></core-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>
<div horizontal justified layout> <div horizontal justified layout>
<state-info flex stateObj="{{stateObj}}"></state-info> <state-info flex stateObj="{{stateObj}}"></state-info>

View File

@ -11,10 +11,6 @@
} }
@media all and (min-width: 1020px) { @media all and (min-width: 1020px) {
:host {
/*padding-bottom: 8px;*/
}
.state-card { .state-card {
width: calc(50% - 44px); width: calc(50% - 44px);
margin: 8px 0 0 8px; margin: 8px 0 0 8px;
@ -24,7 +20,6 @@
@media all and (min-width: 1356px) { @media all and (min-width: 1356px) {
.state-card { .state-card {
width: calc(33% - 38px); width: calc(33% - 38px);
} }
} }
@ -40,7 +35,7 @@
border-radius: 2px; border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px; box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
padding: 16px; padding: 16px;
margin: 0 auto; margin: 16px auto;
} }
</style> </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({ Polymer({
stateObj: {}, stateObj: {},
stateHistory: null, stateHistory: null,
hasHistoryComponent: false,
observe: { observe: {
'stateObj.attributes': 'reposition' 'stateObj.attributes': 'reposition'
@ -42,6 +43,10 @@ Polymer({
window.hass.stateHistoryStore.removeChangeListener(this.stateHistoryStoreChanged); window.hass.stateHistoryStore.removeChangeListener(this.stateHistoryStoreChanged);
}, },
componentStoreChanged: function() {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
stateHistoryStoreChanged: function() { stateHistoryStoreChanged: function() {
if (this.stateObj.entityId) { if (this.stateObj.entityId) {
this.stateHistory = window.hass.stateHistoryStore.get(this.stateObj.entityId); this.stateHistory = window.hass.stateHistoryStore.get(this.stateObj.entityId);
@ -53,7 +58,7 @@ Polymer({
stateObjChanged: function() { stateObjChanged: function() {
if (this.stateObj.entityId && if (this.stateObj.entityId &&
window.hass.stateHistoryStore.isStale(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(); this.stateHistoryStoreChanged();

View File

@ -51,8 +51,29 @@
return moment(this.lastChangedAsDate).fromNow(); 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> </script>
<polymer-element name="home-assistant-api" attributes="auth"> <polymer-element name="home-assistant-api" attributes="auth">
@ -63,30 +84,11 @@
<script> <script>
Polymer({ Polymer({
ready: function() { ready: function() {
var state, var getState = window.hass.stateStore.get;
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);
};
dispatcher.register(function(payload) { dispatcher.register(function(payload) {
switch (payload.actionType) { switch (payload.actionType) {
case actions.ACTION_SHOW_TOAST: case constants.ACTION_SHOW_TOAST:
this.$.toast.text = payload.message; this.$.toast.text = payload.message;
this.$.toast.show(); this.$.toast.show();
break; break;
@ -101,10 +103,9 @@
// if auth was given, tell the backend // if auth was given, tell the backend
if(this.auth) { if(this.auth) {
window.hass.authActions.validate(this.auth); window.hass.uiActions.validateAuth(this.auth);
} }
}, },
}); });
</script> </script>
</polymer-element> </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-call-service.html">
<link rel="import" href="../layouts/partial-dev-set-state.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"> <polymer-element name="home-assistant-main">
<template> <template>
<core-style ref="ha-headers"></core-style> <core-style ref="ha-headers"></core-style>
@ -48,12 +50,15 @@
min-height: 53px; min-height: 53px;
} }
.seperator { .text {
padding: 16px; padding: 16px;
font-size: 14px;
border-top: 1px solid #e0e0e0; border-top: 1px solid #e0e0e0;
} }
.label {
font-size: 14px;
}
.dev-tools { .dev-tools {
padding: 0 8px; padding: 0 8px;
} }
@ -85,12 +90,17 @@
<div flex></div> <div flex></div>
<paper-item id="logout" on-click="{{handleLogOutClick}}"> <paper-item on-click="{{handleLogOutClick}}">
<core-icon icon="exit-to-app"></core-icon> <core-icon icon="exit-to-app"></core-icon>
Log Out Log Out
</paper-item> </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> <div class='dev-tools' layout horizontal justified>
<paper-icon-button <paper-icon-button
icon="settings-remote" data-panel='call-service' icon="settings-remote" data-panel='call-service'
@ -131,26 +141,37 @@
</template> </template>
<script> <script>
Polymer({ var componentStore = window.hass.componentStore;
var streamStore = window.hass.streamStore;
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
selected: "states", selected: "states",
narrow: false, narrow: false,
hasHistoryComponent: false, hasHistoryComponent: false,
isStreaming: false,
hasStreamError: false,
ready: function() { ready: function() {
this.togglePanel = this.togglePanel.bind(this); this.togglePanel = this.togglePanel.bind(this);
this.componentStoreChanged = this.componentStoreChanged.bind(this);
window.hass.componentStore.addChangeListener(this.componentStoreChanged); this.listenToStores(true);
this.componentStoreChanged();
}, },
detached: function() { detached: function() {
window.hass.componentStore.removeChangeListener(this.componentStoreChanged); this.stopListeningToStores();
}, },
componentStoreChanged: function() { 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) { menuSelect: function(ev, detail, sender) {
@ -185,6 +206,6 @@ Polymer({
window.hass.authActions.logOut(); window.hass.authActions.logOut();
}, },
}); }, storeListenerMixIn));
</script> </script>
</polymer-element> </polymer-element>

View File

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

View File

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

View File

@ -111,3 +111,17 @@
word-break: break-all; word-break: break-all;
} }
</core-style> </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>