diff --git a/config.py.example b/config.py.example
new file mode 100644
index 0000000..0adc38a
--- /dev/null
+++ b/config.py.example
@@ -0,0 +1,30 @@
+from mqtt_dash.widgets import column_layout, row_layout, slider_widget, \
+ label_widget, button_widget, log_widget
+
+URL = "https://dashboard.example.com"
+MQTT_BROKER = "ws://example.com/mqtt"
+
+PAGE_TITLE = 'MQTT Dashboard'
+
+WIDGETS = [
+ column_layout("Light", [
+ slider_widget(room,
+ f"hue/set/lights/{room}",
+ f"hue/status/lights/{room}",
+ value_path='val')
+ for room in ('livingroom', 'bedroom', 'kitchen')
+ ]),
+ row_layout("Temperature", [
+ label_widget(f"home/sensors/{room}/temperature", room, "°C")
+ for room in ('livingroom', 'bedroom', 'kitchen')
+ ]),
+ row_layout("Device logs", [
+ log_widget(f"home/devices/{ip}/log", name)
+ for ip, name in [("192.168.1.214",
+ "Bedroom lamp"), ("192.168.1.213", "Sensor office"),
+ ("192.168.1.212", "Sensor livingroom"
+ ), ("192.168.1.211", "Sensor bedroom")]
+ ]),
+ row_layout("Some buttons", button_widget("test", "set to 1", 1),
+ button_widget("test", "set to foo", 'foo'))
+]
diff --git a/default.nix b/default.nix
index 052acbb..f912207 100644
--- a/default.nix
+++ b/default.nix
@@ -1,6 +1,6 @@
{ python3 }:
with python3.pkgs;
-buildPythonApplication rec {
+buildPythonPackage rec {
name = "mqtt-dash";
src = ./.;
propagatedBuildInputs = [ flask ];
diff --git a/mqtt-dash b/mqtt-dash
index a2bf26b..f04043a 100755
--- a/mqtt-dash
+++ b/mqtt-dash
@@ -1,9 +1,28 @@
#!/usr/bin/env python
+import argparse
import os
+import sys
from mqtt_dash.app import app
+args = argparse.ArgumentParser()
+
+args.add_argument('config', default='config.py', help='Configuration file')
+
+options = args.parse_args()
app.secret_key = os.urandom(12)
+app.config.from_pyfile(os.path.abspath(options.config))
+
+
+missing_config_fields = False
+for opt in ('URL', 'MQTT_BROKER', 'PAGE_TITLE', 'WIDGETS'):
+ if opt not in app.config:
+ print(f'Error: field {opt} missing in configuration')
+ missing_config_fields = True
+if missing_config_fields:
+ sys.exit(1)
+
+
app.run(debug=False, host='0.0.0.0', port=4000)
diff --git a/mqtt_dash/app.py b/mqtt_dash/app.py
index faa302d..89872df 100755
--- a/mqtt_dash/app.py
+++ b/mqtt_dash/app.py
@@ -1,201 +1,13 @@
#!/usr/bin/env python
-from flask import Flask, flash, redirect, render_template_string
-from flask import render_template, request, session, abort
-import uuid
+from flask import Flask, render_template
app = Flask(__name__)
-def make_id():
- return uuid.uuid4()
-
-
-def make_widget(template,
- widget_type,
- sub_topic=None,
- extra_classes=None,
- **kwargs):
- def f():
- _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'
\n{template}\n
'
- return render_template_string(widget_template, **kwargs)
-
- return f
-
-
-def button_widget(topic, label, pub_value):
- template = '''
-
- '''
- 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 = '''
-
-
- -{{unit}}
-
- {{label}}
- '''
- 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=''):
- template = '''
-
- -
- {{unit}}
-
- {{label}}
- '''
- return make_widget(template,
- 'label',
- sub_topic=topic,
- topic=topic,
- unit=unit,
- label=label)
-
-
-def log_widget(topic, label):
- id_ = make_id()
- template = '''
-
- {{label}}
- '''
- return make_widget(template,
- "log",
- sub_topic=topic,
- topic=topic,
- label=label,
- id_=id_)
-
-
-def row_layout(title, *elems):
- def f():
- template = '''
-
- {% if title %}
-
{{title}}
- {% endif %}
-
- {% for w in elems %}
-
- {{w()|safe}}
-
- {% endfor %}
-
-
- '''
- return render_template_string(template, title=title, elems=elems)
-
- return f
-
-
-def column_layout(title, *elems):
- def f():
- template = '''
- {% if title %}
- {{title}}
- {% endif %}
-
- {% for w in elems %}
-
- {{w()|safe}}
-
- {% endfor %}
-
- '''
- return render_template_string(template, title=title, elems=elems)
-
- return f
-
-
@app.route('/')
def home():
- return render_template(
- 'base.html',
- widgets=[
- 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/override",
- "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",
- "°C"),
- label_widget("home/sensors/bedroom/temperature", "Bedroom",
- "°C")),
- row_layout(
- "Device logs", log_widget("test", "test log"), *[
- log_widget(f"home/devices/{ip}/log", name)
- for ip, name in [("192.168.1.214", "Bedroom lamp"),
- ("192.168.1.213", "Sensor office"),
- ("192.168.1.212", "Sensor livingroom"),
- ("192.168.1.211", "Sensor bedroom")]
- ]),
- row_layout("Some buttons", button_widget("test", "set to 1", 1),
- button_widget("test", "set to foo", 'foo'))
- ])
+ return render_template('base.html',
+ widgets=app.config['WIDGETS'],
+ mqtt_broker=app.config['MQTT_BROKER'],
+ page_title=app.config['PAGE_TITLE'])
diff --git a/mqtt_dash/static/main.js b/mqtt_dash/static/main.js
new file mode 100644
index 0000000..663e1c6
--- /dev/null
+++ b/mqtt_dash/static/main.js
@@ -0,0 +1,148 @@
+ // Called after form input is processed
+ function startConnect(brokerUri) {
+ // Generate a random client ID
+ clientId = "clientId-" + parseInt(Math.random() * 100);
+ document.getElementById("wrapper").classList.add("wrapper-error");
+ document.getElementById("wrapper").classList.remove("wrapper-ok");
+
+ // Print output for the user in the messages div
+ document.getElementById("messages").innerHTML +=
+ 'Connecting to: ' + brokerUri+ '
';
+ document.getElementById("messages").innerHTML +=
+ 'Using the following client value: ' + clientId + '
';
+
+ // Initialize new Paho client connection
+ client = new Paho.MQTT.Client(brokerUri, clientId=clientId);
+
+ // Set callback handlers
+ client.onConnectionLost = onConnectionLost;
+ client.onMessageArrived = onMessageArrived;
+
+ // Connect the client, if successful, call onConnect function
+ client.connect({
+ onSuccess: onConnect,
+ });
+ }
+
+ // Called when the client connects
+ function onConnect() {
+ document.getElementById("wrapper").classList.remove("wrapper-error");
+ document.getElementById("wrapper").classList.add("wrapper-ok");
+ var subWidgets = document.getElementsByClassName("subscriber");
+ var topics = new Set()
+ for (var i = 0; i < subWidgets.length; i++) {
+ var c = subWidgets.item(i);
+ 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) {
+ document.getElementById("wrapper").classList.remove("wrapper-ok");
+ document.getElementById("wrapper").classList.add("wrapper-error");
+ if (responseObject.errorCode !== 0) {
+ console.log("onConnectionLost: " + responseObject.errorMessage);
+ }
+ }
+
+ 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) {
+ var views = document.getElementsByClassName(message.destinationName + "-value");
+ for (var i = 0 ; i < views.length; i++) {
+ 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 + "
";
+ view.scrollTop = view.scrollHeight;
+ } else if(view.classList.contains("update-policy-add-row")) {
+ var now = (new Date()).toLocaleTimeString();
+ view.innerHTML = ""+ now + " | " +message.payloadString + " |
" + 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();
+ }
+ }
+
+ var widgets = document.querySelectorAll("[data-sub-topic='" + message.destinationName + "']");
+ for (var i = 0 ; i < widgets.length; i++) {
+ widgets.item(i).classList.remove("widget-unset");
+ }
+
+ }
+
+ // Called when the disconnection button is pressed
+ function startDisconnect() {
+ client.disconnect();
+ document.getElementById("messages").innerHTML += 'Disconnected
';
+ updateScroll(); // Scroll to bottom of window
+ }
+
+ // Updates #messages div to auto-scroll
+ function updateScroll() {
+ var element = document.getElementById("messages");
+ element.scrollTop = element.scrollHeight;
+ }
+
+ function publish(topic, value) {
+ var message = new Paho.MQTT.Message(value);
+ message.destinationName = topic;
+ client.send(message);
+ }
+
+ function updateContents(id, value) {
+ elem = document.getElementById(id);
+ elem.innerHTML = value;
+ }
+
+ function init(brokerUri) {
+ startConnect(brokerUri);
+
+ 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 = "";
+ };
+ }
+
+
+ }
diff --git a/mqtt_dash/static/style.css b/mqtt_dash/static/style.css
new file mode 100644
index 0000000..9d59990
--- /dev/null
+++ b/mqtt_dash/static/style.css
@@ -0,0 +1,60 @@
+.widget-unset { color: #888; /* #708ea4; */ }
+#wrapper { 100%;}
+html, body {
+ height: 100%;
+ overflow: auto;
+}
+.wrapper-error { background-color: #fee; }
+#wrapper { width: 90%; height: 100%;}
+.button {
+ 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;
+}
diff --git a/mqtt_dash/templates/base.html b/mqtt_dash/templates/base.html
index 7a4541c..32ae27c 100644
--- a/mqtt_dash/templates/base.html
+++ b/mqtt_dash/templates/base.html
@@ -5,246 +5,15 @@
-
+
+
+
- 0xee home
+ window.onload = function() { init("{{mqtt_broker}}")};
+
+ {{page_title}}
diff --git a/mqtt_dash/widgets.py b/mqtt_dash/widgets.py
new file mode 100644
index 0000000..686e47a
--- /dev/null
+++ b/mqtt_dash/widgets.py
@@ -0,0 +1,159 @@
+import uuid
+
+from flask import render_template_string
+
+
+def make_id():
+ return uuid.uuid4()
+
+
+def make_widget(template,
+ widget_type,
+ sub_topic=None,
+ extra_classes=None,
+ **kwargs):
+ def f():
+ _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'
\n{template}\n
'
+ return render_template_string(widget_template, **kwargs)
+
+ return f
+
+
+def button_widget(topic, label, pub_value):
+ template = '''
+
+ '''
+ 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 = '''
+
+
+ -
+ {{unit}}
+
+
{{label}}
+ '''
+ 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=''):
+ template = '''
+
+ -
+ {{unit}}
+
+
{{label}}
+ '''
+ return make_widget(template,
+ 'label',
+ sub_topic=topic,
+ topic=topic,
+ unit=unit,
+ label=label)
+
+
+def log_widget(topic, label):
+ id_ = make_id()
+ template = '''
+
+
{{label}}
+
+
+ '''
+ return make_widget(template,
+ "log",
+ sub_topic=topic,
+ topic=topic,
+ label=label,
+ id_=id_)
+
+
+def row_layout(title, elems):
+ def f():
+ template = '''
+
+ {% if title %}
+
{{title}}
+ {% endif %}
+
+ {% for w in elems %}
+
+ {{w()|safe}}
+
+ {% endfor %}
+
+
+ '''
+ return render_template_string(template, title=title, elems=elems)
+
+ return f
+
+
+def column_layout(title, elems):
+ def f():
+ template = '''
+ {% if title %}
+
{{title}}
+ {% endif %}
+
+ {% for w in elems %}
+
+ {{w()|safe}}
+
+ {% endfor %}
+
+ '''
+ return render_template_string(template, title=title, elems=elems)
+
+ return f
diff --git a/setup.py b/setup.py
index 59bac34..3b42e70 100644
--- a/setup.py
+++ b/setup.py
@@ -10,5 +10,5 @@ setup(
author_email='mqtt-dash@0xee.eu',
packages=['mqtt_dash'],
scripts=['mqtt-dash'],
- package_data={'mqtt_dash': ['templates/*.html']},
+ package_data={'mqtt_dash': ['templates/*.html', 'static/*']},
)