More widgets and initial page design
parent
42593dc0ac
commit
6e762316c8
190
app.py
190
app.py
|
@ -3,61 +3,125 @@
|
|||
from flask import Flask, flash, redirect, render_template_string
|
||||
from flask import render_template, request, session, abort
|
||||
import os
|
||||
import uuid
|
||||
|
||||
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():
|
||||
template = '''
|
||||
<div>
|
||||
<input type="button" value="{{label}}" onclick="publish('{{topic}}', '{{pub_value}}');"/>
|
||||
</div>
|
||||
'''
|
||||
return render_template_string(
|
||||
template, topic=topic, label=label, pub_value=pub_value)
|
||||
_classes = ['widget'] + list(extra_classes or [])
|
||||
_attrs = {}
|
||||
if sub_topic:
|
||||
_classes.append('subscriber')
|
||||
_attrs['data-sub-topic'] = sub_topic
|
||||
_classes.append(f'{widget_type}-widget')
|
||||
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
|
||||
|
||||
|
||||
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 f():
|
||||
template = '''
|
||||
<div class="widget subscriber" data-topic="{{topic}}">
|
||||
<div>
|
||||
<span class="{{topic}}-value update-policy-replace-content">-</span><span>{{unit}}</span>
|
||||
</div>
|
||||
<div>{{label}}</div>
|
||||
</div>
|
||||
'''
|
||||
return render_template_string(
|
||||
template, topic=topic, label=label, unit=unit)
|
||||
|
||||
return f
|
||||
template = '''
|
||||
<div align="center" class="is-size-2">
|
||||
<span class="{{topic}}-value update-policy-replace-content">-</span>
|
||||
<span>{{unit}}</span>
|
||||
</div>
|
||||
<div>{{label}}</div>
|
||||
'''
|
||||
return make_widget(
|
||||
template,
|
||||
'label',
|
||||
sub_topic=topic,
|
||||
topic=topic,
|
||||
unit=unit,
|
||||
label=label)
|
||||
|
||||
|
||||
def log_widget(topic, label):
|
||||
def f():
|
||||
template = '''
|
||||
<div class="widget subscriber" data-topic="{{topic}}">
|
||||
<div class="{{topic}}-value update-policy-append-content" style="overflow: auto; height:100px;">
|
||||
</div>
|
||||
<div>{{label}}</div>
|
||||
</div>
|
||||
'''
|
||||
return render_template_string(template, topic=topic, label=label)
|
||||
|
||||
return f
|
||||
id_ = make_id()
|
||||
template = '''
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-hoverable is-fullwidth"
|
||||
style="overflow: auto; height:100px;">
|
||||
<thead><tr>
|
||||
<th>Time</th><th>Message</th>
|
||||
</tr></thead>
|
||||
<tbody id="{{id_}}-content" class="{{topic}}-value update-policy-add-row">
|
||||
</tbody>
|
||||
</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 f():
|
||||
template = '''
|
||||
<div>
|
||||
<div>{{title}}</div>
|
||||
<div style="display: flex;">
|
||||
<div class="box">
|
||||
{% if title %}
|
||||
<div class="title">{{title}}</div>
|
||||
{% endif %}
|
||||
<div class="columns is-tablet">
|
||||
{% for w in elems %}
|
||||
{{w()|safe}}
|
||||
<div class="column">
|
||||
{{w()|safe}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,24 +131,66 @@ def row_layout(title, *elems):
|
|||
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('/')
|
||||
def home():
|
||||
return render_template(
|
||||
'base.html',
|
||||
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(
|
||||
"Temperature",
|
||||
label_widget("home/sensors/livingroom/temperature",
|
||||
"livingroom", "°C"),
|
||||
label_widget("home/sensors/office/temperature", "office",
|
||||
"Livingroom", "°C"),
|
||||
label_widget("home/sensors/office/temperature", "Office",
|
||||
"°C"),
|
||||
label_widget("home/sensors/bedroom/temperature", "bedroom",
|
||||
label_widget("home/sensors/bedroom/temperature", "Bedroom",
|
||||
"°C")),
|
||||
row_layout("Device logs", log_widget("test", "test log"),
|
||||
log_widget("test2", "test log 2")),
|
||||
button_widget("test", "set to 1", 1),
|
||||
button_widget("test", "set to foo", 'foo')
|
||||
row_layout("Some buttons", button_widget("test", "set to 1", 1),
|
||||
button_widget("test", "set to foo", 'foo'))
|
||||
])
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
with import <nixpkgs> {}; with python3Packages;
|
||||
|
||||
buildPythonPackage rec {
|
||||
name = "mqtt-dash";
|
||||
src = ./.;
|
||||
propagatedBuildInputs = [ flask ];
|
||||
}
|
|
@ -1,15 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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">
|
||||
.widget { width: 100%; padding: 5px; margin: 10px; background-color: white; }
|
||||
.widget-unset { color: gray; }
|
||||
#wrapper { width: 100%; height: 100%;}
|
||||
html, body { height: 100%;}
|
||||
.wrapper-ok { background-color: #e9ecef;}
|
||||
.wrapper-error { background-color: #ff8888;}
|
||||
/* background-color: #486e88; color: #e0e0e0; */}
|
||||
.widget-unset { color: #888; /* #708ea4; */ }
|
||||
#wrapper { 100%;}
|
||||
html, body {
|
||||
height: 100%;
|
||||
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>
|
||||
<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>
|
||||
// Called after form input is processed
|
||||
|
@ -44,22 +122,23 @@
|
|||
function onConnect() {
|
||||
document.getElementById("wrapper").classList.remove("wrapper-error");
|
||||
document.getElementById("wrapper").classList.add("wrapper-ok");
|
||||
|
||||
var subWidgets = document.getElementsByClassName("subscriber");
|
||||
console.log("Found " + subWidgets.length + " widgets");
|
||||
var topics = new Set()
|
||||
for (var i = 0; i < subWidgets.length; i++) {
|
||||
var c = subWidgets.item(i);
|
||||
var topic = c.getAttribute('data-topic');
|
||||
console.log("Subscribing to: " + topic);
|
||||
client.subscribe(topic);
|
||||
var topic = c.getAttribute('data-sub-topic');
|
||||
topics.add(topic);
|
||||
c.classList.add('widget-unset');
|
||||
}
|
||||
for (let topic of topics)
|
||||
{
|
||||
client.subscribe(topic);
|
||||
}
|
||||
// Subscribe to the requested topic
|
||||
}
|
||||
|
||||
// Called when the client loses its connection
|
||||
function onConnectionLost(responseObject) {
|
||||
console.log("onConnectionLost: Connection Lost");
|
||||
document.getElementById("wrapper").classList.remove("wrapper-ok");
|
||||
document.getElementById("wrapper").classList.add("wrapper-error");
|
||||
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
|
||||
function onMessageArrived(message) {
|
||||
console.log(message.destinationName + "-value");
|
||||
var views = document.getElementsByClassName(message.destinationName + "-value");
|
||||
for (var i = 0 ; i < views.length; i++) {
|
||||
view = views.item(i);
|
||||
console.log(view);
|
||||
var view = views.item(i);
|
||||
if (view.classList.contains("update-policy-replace-content")) {
|
||||
view.innerHTML = message.payloadString;
|
||||
} else if(view.classList.contains("update-policy-append-content")) {
|
||||
view.innerHTML += message.payloadString + "<br/>";
|
||||
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;
|
||||
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>
|
||||
<title>0xee home</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<br/>
|
||||
<div id="value">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<div id="wrapper" align="center">
|
||||
<br/>
|
||||
<div id="widgets">
|
||||
{% for w in widgets %}
|
||||
{{w()|safe}}
|
||||
|
@ -127,5 +260,7 @@
|
|||
<input type="button" onclick="startConnect()" value="Connect">
|
||||
<input type="button" onclick="startDisconnect()" value="Disconnect">
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in New Issue