More widgets and initial page design

This commit is contained in:
0xee 2019-10-24 21:29:54 +02:00
parent 42593dc0ac
commit 6e762316c8
3 changed files with 312 additions and 64 deletions

190
app.py
View File

@ -3,61 +3,125 @@
from flask import Flask, flash, redirect, render_template_string from flask import Flask, flash, redirect, render_template_string
from flask import render_template, request, session, abort from flask import render_template, request, session, abort
import os import os
import uuid
app = Flask(__name__) app = Flask(__name__)
def button_widget(topic, label, pub_value): def make_id():
return uuid.uuid4()
def make_widget(template,
widget_type,
sub_topic=None,
extra_classes=None,
**kwargs):
def f(): def f():
template = ''' _classes = ['widget'] + list(extra_classes or [])
<div> _attrs = {}
<input type="button" value="{{label}}" onclick="publish('{{topic}}', '{{pub_value}}');"/> if sub_topic:
</div> _classes.append('subscriber')
''' _attrs['data-sub-topic'] = sub_topic
return render_template_string( _classes.append(f'{widget_type}-widget')
template, topic=topic, label=label, pub_value=pub_value) kwargs['sub_topic'] = sub_topic
_attrs['class'] = ' '.join([c for c in _classes if c is not None])
attr_str = " ".join([f'{k}="{v}"' for k, v in _attrs.items()])
widget_template = f'<div {attr_str}>\n{template}\n</div>'
return render_template_string(widget_template, **kwargs)
return f return f
def button_widget(topic, label, pub_value):
template = '''
<input type="button" class="button is-large is-info is-outlined" value="{{label}}" onclick="publish('{{topic}}', '{{pub_value}}');"/>
'''
return make_widget(
template, 'button', label=label, pub_value=pub_value, topic=topic)
def slider_widget(label,
topic,
sub_topic=None,
value_path=None,
unit='',
min_val=0,
max_val=255):
id_ = make_id()
sub_topic = sub_topic or topic
template = '''
<div style="display:flex;justify-content:center;align-items:center;height:60px;">
<input id="{{id_}}" class="slider update-policy-update-value {{sub_topic}}-value"
data-pub-topic="{{topic}}"
{% if value_path %} data-value-path="{{value_path}}" {% endif %}
min="{{min_val}}" type="range" max="{{max_val}}"
value="{{min_val}}"/>
<span id="{{id_}}-textual" class="slider-value">-</span><span>{{unit}}</span>
</div>
<div>{{label}}</div>
'''
return make_widget(
template,
'slider',
sub_topic=sub_topic,
label=label,
min_val=min_val,
max_val=max_val,
value_path=value_path,
topic=topic,
unit=unit,
id_=id_)
def label_widget(topic, label, unit=''): def label_widget(topic, label, unit=''):
def f(): template = '''
template = ''' <div align="center" class="is-size-2">
<div class="widget subscriber" data-topic="{{topic}}"> <span class="{{topic}}-value update-policy-replace-content">-</span>
<div> <span>{{unit}}</span>
<span class="{{topic}}-value update-policy-replace-content">-</span><span>{{unit}}</span> </div>
</div> <div>{{label}}</div>
<div>{{label}}</div> '''
</div> return make_widget(
''' template,
return render_template_string( 'label',
template, topic=topic, label=label, unit=unit) sub_topic=topic,
topic=topic,
return f unit=unit,
label=label)
def log_widget(topic, label): def log_widget(topic, label):
def f(): id_ = make_id()
template = ''' template = '''
<div class="widget subscriber" data-topic="{{topic}}"> <div class="table-container">
<div class="{{topic}}-value update-policy-append-content" style="overflow: auto; height:100px;"> <table class="table is-striped is-hoverable is-fullwidth"
</div> style="overflow: auto; height:100px;">
<div>{{label}}</div> <thead><tr>
</div> <th>Time</th><th>Message</th>
''' </tr></thead>
return render_template_string(template, topic=topic, label=label) <tbody id="{{id_}}-content" class="{{topic}}-value update-policy-add-row">
</tbody>
return f </table>
</div>
<div>{{label}}<button id="{{id_}}" class="delete log-delete"></button></div>
'''
return make_widget(
template, "log", sub_topic=topic, topic=topic, label=label, id_=id_)
def row_layout(title, *elems): def row_layout(title, *elems):
def f(): def f():
template = ''' template = '''
<div> <div class="box">
<div>{{title}}</div> {% if title %}
<div style="display: flex;"> <div class="title">{{title}}</div>
{% endif %}
<div class="columns is-tablet">
{% for w in elems %} {% for w in elems %}
{{w()|safe}} <div class="column">
{{w()|safe}}
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -67,24 +131,66 @@ def row_layout(title, *elems):
return f return f
def column_layout(title, *elems):
def f():
template = '''
{% if title %}
<div class="title">{{title}}</div>
{% endif %}
<div class="rows">
{% for w in elems %}
<div class="row">
{{w()|safe}}
</div>
{% endfor %}
</div>
'''
return render_template_string(template, title=title, elems=elems)
return f
@app.route('/') @app.route('/')
def home(): def home():
return render_template( return render_template(
'base.html', 'base.html',
widgets=[ widgets=[
label_widget("test", "test value", "%"), column_layout(
"Light",
slider_widget(
"Küche",
"hue/set/lights/Ku",
"hue/status/lights/Ku",
value_path='val'),
slider_widget(
"WZ1",
"hue/set/lights/WZ1",
"hue/status/lights/WZ1",
value_path='val'),
slider_widget(
"WZ2",
"hue/set/lights/WZ2",
"hue/status/lights/WZ2",
value_path='val'),
slider_widget(
"Vorzimmer",
"hue/set/lights/Vorzimmer",
"hue/status/lights/Vorzimmer",
value_path='val'),
slider_widget("Schlafzimmer",
"home/devices/192.168.1.214/brightness")),
row_layout( row_layout(
"Temperature", "Temperature",
label_widget("home/sensors/livingroom/temperature", label_widget("home/sensors/livingroom/temperature",
"livingroom", "°C"), "Livingroom", "°C"),
label_widget("home/sensors/office/temperature", "office", label_widget("home/sensors/office/temperature", "Office",
"°C"), "°C"),
label_widget("home/sensors/bedroom/temperature", "bedroom", label_widget("home/sensors/bedroom/temperature", "Bedroom",
"°C")), "°C")),
row_layout("Device logs", log_widget("test", "test log"), row_layout("Device logs", log_widget("test", "test log"),
log_widget("test2", "test log 2")), log_widget("test2", "test log 2")),
button_widget("test", "set to 1", 1), row_layout("Some buttons", button_widget("test", "set to 1", 1),
button_widget("test", "set to foo", 'foo') button_widget("test", "set to foo", 'foo'))
]) ])

7
default.nix Normal file
View File

@ -0,0 +1,7 @@
with import <nixpkgs> {}; with python3Packages;
buildPythonPackage rec {
name = "mqtt-dash";
src = ./.;
propagatedBuildInputs = [ flask ];
}

View File

@ -1,15 +1,93 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=0.8"/>
<!-- http://paletton.com/#uid=13w0u0kllllaFw0g0qFqFg0w0aF -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css">
<style type="text/css"> <style type="text/css">
.widget { width: 100%; padding: 5px; margin: 10px; background-color: white; } /* background-color: #486e88; color: #e0e0e0; */}
.widget-unset { color: gray; } .widget-unset { color: #888; /* #708ea4; */ }
#wrapper { width: 100%; height: 100%;} #wrapper { 100%;}
html, body { height: 100%;} html, body {
.wrapper-ok { background-color: #e9ecef;} height: 100%;
.wrapper-error { background-color: #ff8888;} overflow: auto;
/* color: #e0e0e0; */
}
.wrapper-ok { /*background-color: #29516D;*/}
.wrapper-error { background-color: #fee; /*#aa5739;*/}
#wrapper { width: 90%; height: 100%;}
.button {
/* text-align: center; */
/* text-decoration: none; */
/* display: inline-block; */
/* font-size: 16px; */
width: 100%;
height: 100%;
}
.log-delete {
vertical-align: 5px;
margin-left:5px;
}
.table-container {
max-height: 10rem;
overflow: auto;
}
.slider {
width: 90%;
}
.slider::-moz-range-thumb {
border: 0px;
height: 24px;
width: 24px;
border-radius: 100px;
background: #3298dc;
cursor: pointer;
}
.slider::-moz-range-progress {
background-color: #3298dc;
height: 12px;
border-radius: 100px;
}
.slider::-moz-range-track {
width: 100%;
height: 12px;
border-radius: 100px;
background-color: #eee;
}
.slider-value {
margin-left:10px;
width: 10%;
}
.button {
-webkit-transition-duration: 0.4s; /* Safari */
transition-duration: 0.4s;
}
.widget {
align: center;
vertical-align: middle;
}
.button:hover {
/* background-color: #123752; */
}
/* .with-border { */
/* border-width: .1rem; */
/* border-color: #ccc; #f8f8f8; */
/* border-style: solid; */
/* padding: 5px; */
/* margin: 10px; */
/* } */
</style> </style>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.min.js" type="text/javascript"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.min.js" type="text/javascript"></script>
<script> <script>
// Called after form input is processed // Called after form input is processed
@ -44,22 +122,23 @@
function onConnect() { function onConnect() {
document.getElementById("wrapper").classList.remove("wrapper-error"); document.getElementById("wrapper").classList.remove("wrapper-error");
document.getElementById("wrapper").classList.add("wrapper-ok"); document.getElementById("wrapper").classList.add("wrapper-ok");
var subWidgets = document.getElementsByClassName("subscriber"); var subWidgets = document.getElementsByClassName("subscriber");
console.log("Found " + subWidgets.length + " widgets"); var topics = new Set()
for (var i = 0; i < subWidgets.length; i++) { for (var i = 0; i < subWidgets.length; i++) {
var c = subWidgets.item(i); var c = subWidgets.item(i);
var topic = c.getAttribute('data-topic'); var topic = c.getAttribute('data-sub-topic');
console.log("Subscribing to: " + topic); topics.add(topic);
client.subscribe(topic);
c.classList.add('widget-unset'); c.classList.add('widget-unset');
} }
for (let topic of topics)
{
client.subscribe(topic);
}
// Subscribe to the requested topic // Subscribe to the requested topic
} }
// Called when the client loses its connection // Called when the client loses its connection
function onConnectionLost(responseObject) { function onConnectionLost(responseObject) {
console.log("onConnectionLost: Connection Lost");
document.getElementById("wrapper").classList.remove("wrapper-ok"); document.getElementById("wrapper").classList.remove("wrapper-ok");
document.getElementById("wrapper").classList.add("wrapper-error"); document.getElementById("wrapper").classList.add("wrapper-error");
if (responseObject.errorCode !== 0) { if (responseObject.errorCode !== 0) {
@ -67,18 +146,34 @@
} }
} }
function getByPath(obj, path) {
var current=obj;
path.split('.').forEach(function(p){ current = current[p]; });
return current;
}
// Called when a message arrives // Called when a message arrives
function onMessageArrived(message) { function onMessageArrived(message) {
console.log(message.destinationName + "-value");
var views = document.getElementsByClassName(message.destinationName + "-value"); var views = document.getElementsByClassName(message.destinationName + "-value");
for (var i = 0 ; i < views.length; i++) { for (var i = 0 ; i < views.length; i++) {
view = views.item(i); var view = views.item(i);
console.log(view);
if (view.classList.contains("update-policy-replace-content")) { if (view.classList.contains("update-policy-replace-content")) {
view.innerHTML = message.payloadString; view.innerHTML = message.payloadString;
} else if(view.classList.contains("update-policy-append-content")) { } else if(view.classList.contains("update-policy-append-content")) {
view.innerHTML += message.payloadString + "<br/>"; view.innerHTML += message.payloadString + "<br/>";
view.scrollTop = view.scrollHeight; view.scrollTop = view.scrollHeight;
} else if(view.classList.contains("update-policy-add-row")) {
var now = (new Date()).toLocaleTimeString();
view.innerHTML = "<tr><td>"+ now + "</td><td>" +message.payloadString + "</td></tr>" + view.innerHTML;
} else if(view.classList.contains("update-policy-update-value")) {
jsonPath = view.getAttribute('data-value-path');
if (jsonPath) {
view.value = getByPath(JSON.parse(message.payloadString),
jsonPath);
} else {
view.value = message.payloadString;
}
view.onchange();
} }
} }
@ -107,16 +202,54 @@
message.destinationName = topic; message.destinationName = topic;
client.send(message); client.send(message);
} }
window.onload = startConnect;
function updateContents(id, value) {
elem = document.getElementById(id);
elem.innerHTML = value;
}
function init() {
startConnect();
var sliders = document.getElementsByClassName("slider");
for (var i = 0 ; i < sliders.length; i++) {
var slider = sliders.item(i);
slider.onchange = function() {
document.getElementById(this.id + "-textual").innerHTML =
this.value;
};
slider.oninput = function() {
this.onchange()
var topic = this.getAttribute('data-pub-topic');
publish(topic, this.value);
};
}
var logDeletes = document.getElementsByClassName("log-delete");
for (var i = 0 ; i < logDeletes.length; i++) {
var elem = logDeletes.item(i);
elem.onclick = function() {
console.log(this.id);
table = document.getElementById(this.id + "-content");
console.log(table.id);
table.innerHTML = "";
};
}
}
window.onload = init;
</script> </script>
<title>0xee home</title> <title>0xee home</title>
</head> </head>
<body> <body>
<div id="wrapper"> <div align="center">
<br/> <div id="wrapper" align="center">
<div id="value"> <br/>
</div>
<div id="widgets"> <div id="widgets">
{% for w in widgets %} {% for w in widgets %}
{{w()|safe}} {{w()|safe}}
@ -127,5 +260,7 @@
<input type="button" onclick="startConnect()" value="Connect"> <input type="button" onclick="startConnect()" value="Connect">
<input type="button" onclick="startDisconnect()" value="Disconnect"> <input type="button" onclick="startDisconnect()" value="Disconnect">
</div> </div>
<br/>
</div>
</body> </body>
</html> </html>