{"id":654,"date":"2022-11-27T23:36:56","date_gmt":"2022-11-27T22:36:56","guid":{"rendered":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/?p=654"},"modified":"2023-06-17T10:13:59","modified_gmt":"2023-06-17T09:13:59","slug":"using-websockets-with-emonpi-to-create-a-live-feed-dashboard","status":"publish","type":"post","link":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/using-websockets-with-emonpi-to-create-a-live-feed-dashboard\/","title":{"rendered":"Using Websockets with EmonPi MQTT Broker to Create a &#8220;Live Feed&#8221; Dashboard"},"content":{"rendered":"<p><em>This is not a complete description of the background tech; there is plenty of info on the web about websockets, mqtt, and the javascript libraries.<\/em><\/p>\n<p>The motivating idea behind this experiment is to be able to have a live-updating dashboard with the minimum set of dependencies of the &#8220;install X&#8221; kind. Since the EmonPi aleady has a MQTT broker, it provides the basis for feeding data to the dashboard. Websockets allows a &#8220;web page&#8221; (it can be a file viewed in your web browser) to send and receive data, and update the page, using JavaScript. This is not particularly difficult, but there are several steps which took a couple of evenings to research and put into practice, so here are some notes (for myself in the future, and anyone else who finds it!).<\/p>\n<p>If you are looking for some &#8220;homework reading&#8221;, a decent place to start is <a href=\"http:\/\/www.steves-internet-guide.com\/using-javascript-mqtt-client-websockets\/\">Steve&#8217;s Internet Guide<\/a>.<\/p>\n<h2>Make Mosquitto Listen for Websockets Connections<\/h2>\n<p>Mosquitto is the MQTT broker on the EmonPi. It is configured to listen for connections which employ the &#8220;mqtt:&#8221; protocol. It is possible to add a websockets listener (&#8220;ws:&#8221; protocol), with the conventional port 9001 assignment as follows:<\/p>\n<ul>\n<li>SSH onto the EmonPi and navigate to \/etc\/mosquitto.<\/li>\n<li>Modify the mosquitto.conf file to add &#8220;listener 9001&#8221; and &#8220;protocol websockets&#8221;, see below. I have also added explicit lines for the default mqtt listener on port 1883 in the interest of clarity, although I believe they are not required. You will need to use &#8220;sudo&#8221;. Alternatively, it should be possible to add a file to the &#8220;include_dir&#8221;.<\/li>\n<li>Restart mosquitto using &#8220;sudo systemctl restart mosquitto.service&#8221;<\/li>\n<\/ul>\n<blockquote>\n<pre>pid_file \/var\/run\/mosquitto.pid\r\npersistence false\r\npersistence_location \/var\/lib\/mosquitto\/\r\nlog_dest file \/var\/log\/mosquitto\/mosquitto.log\r\ninclude_dir \/etc\/mosquitto\/conf.d\r\n\r\nlistener 1883\r\nprotocol mqtt\r\n\r\nlistener 9001\r\nprotocol websockets\r\n\r\nallow_anonymous false\r\npassword_file \/etc\/mosquitto\/passwd\r\nlog_type error<\/pre>\n<\/blockquote>\n<p>It should now be possible to test two things: that the existing mqtt protocol listener, which is relied on to service data to EmonCMS, and that the websockets listener us &#8220;up&#8221;. I used &#8220;MQTT Explorer&#8221;, a free and simple client, which should be set up to <em>not<\/em> validate a certificate and to have an empty &#8220;Basepath&#8221; (it defaults to &#8220;ws&#8221;).<\/p>\n<h2>Create the Websockets Dashboard with HTML and JavaScript<\/h2>\n<p>My primary aim for this experiment is to be able to co-opt EmonPi to broker air quality data from a home-brew particulate matter, VOC, NOx, CO2 sensor combo, but I&#8217;m using the existing emon data to demonstrate the concept, which boils down to &#8220;guages&#8221; using the Google Charts toolkit, and scrolling line charts using Chart.js.<\/p>\n<p>Here is the code to hack about with, based on snippets from various places, with modifications and updating to a recent. It should just live in a plain text file with a &#8220;.html&#8221; extension, and can be opened in your web browser. It is not beautiful but demonstrates the concept. There is some logging to the &#8220;console&#8221;, which is where error messages will also appear. Hit F12 on Firefox or Edge (or Chrome too I think) to find the console.<\/p>\n<div>\n<blockquote>\n<pre>&lt;!DOCTYPE html&gt;\r\n&lt;html lang=\"en\"&gt;\r\n&lt;head&gt;\r\n\u00a0 \u00a0 &lt;meta charset=\"UTF-8\"&gt;\r\n  \u00a0 &lt;!-- This helps with viewing on mobile devices --&gt;\r\n\u00a0 \u00a0 &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"&gt;\r\n\u00a0 \u00a0 &lt;title&gt;Real-Time Charts&lt;\/title&gt;\r\n  \u00a0 &lt;!-- Google charts --&gt;\r\n\u00a0 \u00a0 &lt;script type=\"text\/javascript\" src=\"https:\/\/www.gstatic.com\/charts\/loader.js\"&gt;&lt;\/script&gt;\r\n  \u00a0 &lt;!-- Chart.js --&gt;\r\n\u00a0 \u00a0 &lt;script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/Chart.js\/3.9.1\/chart.min.js\" integrity=\"sha512-ElRFoEQdI5Ht6kZvyzXhYG9NqjtkmlkfYk0wr6wHxU9JEHakS7UJZNeml5ALk+8IKlU6jDgMabC3vkumRokgJA==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"&gt;&lt;\/script&gt;\r\n  \u00a0 &lt;!-- The Paho Javascript for MQTT over Websockets --&gt;\r\n\u00a0 \u00a0 &lt;script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/paho-mqtt\/1.0.1\/mqttws31.min.js\" type=\"text\/javascript\"&gt;&lt;\/script&gt;\r\n\u00a0 \u00a0 &lt;!-- Bootstrap CSS - can be removed but will help with the styling --&gt;\r\n\u00a0 \u00a0 &lt;link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/bootstrap\/5.2.3\/css\/bootstrap.min.css\" integrity=\"sha512-SbiR\/eusphKoMVVXysTKG\/7VseWii+Y3FdHrt0EpKgpToZeemhqHeZeLWLhJutz\/2ut2Vw1uQEj2MbRF+TVBUA==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" \/&gt;\r\n\u00a0 \u00a0 &lt;link rel=\"stylesheet\" type=\"text\/css\" href=\"{{ url_for('static', filename='main.css') }}\"&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n&lt;script type=\"text\/javascript\"&gt;\r\n\u00a0 \u00a0 \/\/ load the google charts stuff and only after it is loaded should we start to set up the data acquisition\r\n\u00a0 \u00a0 \/\/ otherwise, we find the charts library is being called upon before it exists!\r\n\u00a0 \u00a0 google.charts.load('current', {'packages':['gauge']});\r\n\u00a0 \u00a0 google.charts.setOnLoadCallback(doWhenReady);\r\n\r\n\u00a0 \u00a0 function doWhenReady() {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ Create a client instance. NB clientid SHOULD BE DIFFERENT between browser clients; the following should work fine in a home environment.\r\n\u00a0 \u00a0 \u00a0 \u00a0 var clientId = \"client-\" + Date.now().toString();\r\n\u00a0 \u00a0 \u00a0 \u00a0 client = new Paho.MQTT.Client(\"192.168.1.1\", 9001, clientId);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ set callback handlers\r\n\u00a0 \u00a0 \u00a0 \u00a0 client.onConnectionLost = onConnectionLost;\r\n\u00a0 \u00a0 \u00a0 \u00a0 client.onMessageArrived = onMessageArrived;\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ connect the client\r\n\u00a0 \u00a0 \u00a0 \u00a0 client.connect({onSuccess:onConnect, userName: \"emonpi\", password: \"emonpimqtt2016\"});\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ called when the client connects\r\n\u00a0 \u00a0 \u00a0 \u00a0 function onConnect() {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Once a connection has been made, make subscriptions\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 console.log(\"onConnect\");\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 client.subscribe(\"emon\/emonpi\/vrms\");\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 client.subscribe(\"emon\/emonpi\/power1\");\r\n\u00a0 \u00a0 \u00a0 \u00a0 }\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ called when the client loses its connection\r\n\u00a0 \u00a0 \u00a0 \u00a0 function onConnectionLost(responseObject) {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (responseObject.errorCode !== 0) {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 console.log(\"onConnectionLost: \" + responseObject.errorMessage);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 document.getElementById(\"status\").innerText = \"Lost connection due to: \" + responseObject.errorMessage;\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 }\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ called when a message arrives. Note that the full topic name is aka \"destinationName\"\r\n\u00a0 \u00a0 \u00a0 \u00a0 function onMessageArrived(message) {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 document.getElementById(\"status\").innerHTML = \"\";\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 console.log(\"onMessageArrived: \" + message.destinationName + \" : \" + message.payloadString);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (message.destinationName == \"emon\/emonpi\/vrms\") {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ \"+\" converts string to numeric, which is then rounded by toFixed(), which returns a string!\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 var vrms = (+message.payloadString).toFixed(2);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Google chart guage\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 vrmsGaugeData.setValue(0, 1, +vrms);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 vrms_gauge.draw(vrmsGaugeData, vrmsOptions);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ scrolling line chart.js\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 if (vrmsChartData.data.labels.length === 20) {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 vrmsChartData.data.labels.shift();\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 vrmsChartData.data.datasets[0].data.shift();\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ we don't have the time in the mqtt message!\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 var timestamp = new Date().toLocaleTimeString();\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 vrmsChartData.data.labels.push(timestamp);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 vrmsChartData.data.datasets[0].data.push(vrms);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 lineChartVrms.update();\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 } else if (message.destinationName == \"emon\/emonpi\/power1\") {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 var power1 = message.payloadString;\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \/\/ Google\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 power1GaugeData.setValue(0, 1, +power1);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 power1_gauge.draw(power1GaugeData, power1Options);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 }\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ ------------- Google Charts -----------\r\n\u00a0 \u00a0 \u00a0 \u00a0 var vrmsGaugeData = new google.visualization.arrayToDataTable([\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ['Label', 'Value'],\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ['Vrms', 240]\r\n\u00a0 \u00a0 \u00a0 \u00a0 ]);\r\n  \u00a0 \u00a0 \u00a0 var power1GaugeData = google.visualization.arrayToDataTable([\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ['Label', 'Value'],\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 ['Power', 0]\r\n\u00a0 \u00a0 \u00a0 \u00a0 ]);\r\n\u00a0 \u00a0 \u00a0 \u00a0 var vrmsOptions = {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 min \u00a0 \u00a0 \u00a0 \u00a0: 200,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 max \u00a0 \u00a0 \u00a0 \u00a0: 260,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 minorTicks : 5,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 greenFrom \u00a0: 230,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 greenTo \u00a0 \u00a0: 250,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 majorTicks : ['200', '210', '220', '230', '240', '250', '260']\r\n\u00a0 \u00a0 \u00a0 \u00a0 };\r\n\u00a0 \u00a0 \u00a0 \u00a0 var power1Options = {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 min \u00a0 \u00a0 \u00a0 \u00a0: 0,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 max \u00a0 \u00a0 \u00a0 \u00a0: 5000,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 minorTicks : 5,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 majorTicks : ['0', '1kW', '2kW', '3kW', '4kW', '5kW']\r\n\u00a0 \u00a0 \u00a0 \u00a0 };\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 var vrms_gauge = new google.visualization.Gauge(document.getElementById('vrms-gauge'));\r\n\u00a0 \u00a0 \u00a0 \u00a0 var power1_gauge = new google.visualization.Gauge(document.getElementById('power1-gauge'));\r\n\u00a0 \u00a0 \u00a0 \u00a0 vrms_gauge.draw(vrmsGaugeData, vrmsOptions);\r\n\u00a0 \u00a0 \u00a0 \u00a0 power1_gauge.draw(power1GaugeData, power1Options);\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ chart.js\r\n\u00a0 \u00a0 \u00a0 \u00a0 var chartOptions = {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 responsive: true,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 tooltips: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 mode: 'index',\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 intersect: false,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 },\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 hover: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 mode: 'nearest',\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 intersect: true\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 },\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 scales: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 xAxes: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 display: true,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 scaleLabel: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 display: true,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 labelString: 'Time'\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 },\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 yAxes: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 display: true,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 scaleLabel: {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 display: true,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 labelString: 'Value'\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }\r\n\u00a0 \u00a0 \u00a0 \u00a0 };\r\n\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ this allows us to have common chart options, while varying the scale in each chart\r\n\u00a0 \u00a0 \u00a0 \u00a0 var vrmsChartOptions = structuredClone(chartOptions);\r\n\u00a0 \u00a0 \u00a0 \u00a0 \/\/ use sensible range, which will be expanded if the data goes outside \"suggested\"\r\n\u00a0 \u00a0 \u00a0 \u00a0 vrmsChartOptions.scales.yAxes.suggestedMin = 230;\r\n\u00a0 \u00a0 \u00a0 \u00a0 vrmsChartOptions.scales.yAxes.suggestedMax = 250;\r\n\r\n  \u00a0 \u00a0 \u00a0 var vrmsChartData = {\r\n\u00a0 \u00a0 \u00a0 \u00a0 type : 'line',\r\n\u00a0 \u00a0 \u00a0 \u00a0 data : {\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 labels: [],\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 datasets : [{\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 label : 'Vrms',\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 backgroundColor : 'rgba(255, 136, 0, 0.5)',\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 borderColor : 'rgba(255, 136, 0, 1.0)',\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 fill: true,\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 data : []\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 }]\r\n\u00a0 \u00a0 \u00a0 \u00a0 },\r\n\u00a0 \u00a0 \u00a0 \u00a0 options : vrmsChartOptions\r\n\u00a0 \u00a0 \u00a0 \u00a0 };\r\n\r\n  \u00a0 \u00a0 \u00a0 const lineChartVrms = new Chart('vrmsChart', vrmsChartData);\r\n\u00a0 \u00a0 }\r\n&lt;\/script&gt;\r\n\r\n&lt;h1&gt;EmonPi Live Feed&lt;\/h1&gt;\r\n&lt;div class=\"container\"&gt;\r\n\u00a0 \u00a0 &lt;div class=\"row\"&gt;\r\n\u00a0 \u00a0 \u00a0 \u00a0 &lt;div id=\"vrms-gauge\" style=\"width: 120px; height: 120px;\"&gt;&lt;\/div&gt;\r\n\u00a0 \u00a0 \u00a0 \u00a0 &lt;div id=\"power1-gauge\" style=\"width: 120px; height: 120px;\"&gt;&lt;\/div&gt;\r\n\u00a0 \u00a0 &lt;\/div&gt;\r\n\u00a0 \u00a0 &lt;div class=\"row\" id=\"status\"&gt;loading data...&lt;\/div&gt;\r\n\u00a0 \u00a0 &lt;div class=\"col-10\"&gt;\r\n\u00a0 \u00a0 &lt;div class=\"card\"&gt;\r\n\u00a0 \u00a0 \u00a0 \u00a0 &lt;div class=\"card-body\"&gt;\r\n\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 &lt;canvas id=\"vrmsChart\"&gt;&lt;\/canvas&gt;\r\n\u00a0 \u00a0 \u00a0 \u00a0 &lt;\/div&gt;\r\n\u00a0 \u00a0 &lt;\/div&gt;\r\n&lt;\/div&gt;\r\n&lt;\/div&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;\r\n<\/pre>\n<\/blockquote>\n<\/div>\n<p>Don&#8217;t forget to change the IP address for the MQTT broker!<\/p>\n<h2>Serving the Dashboard<\/h2>\n<p>While the above HTML + JavaScript works fine <em>as a file on your PC <\/em>(so long as you have a network connection to acquire all those JavaScript libraries from), it can also be placed on the EmonPi.<\/p>\n<p>I have opted to create a separate area from EmonCMS in the interest of avoiding too much risk of cock-up, future muddle, etc&#8230; The files in this separate area will all be &#8220;static&#8221; in the sense that they are just served to users as they are (no PHP etc). The dashboard page above IS static in this sense, since the JavaScript executes on your PC inside the web browser; all the EmonPi does is send it.<\/p>\n<p>EmonCMS uses the Apache2 web server, so it is easy to make it listen on a different port (EmonCMS uses port 80, and I have chosen port 8001) using the following commands (in a SSH).<\/p>\n<p><strong>Create the place where the HTML file will live<\/strong><\/p>\n<blockquote>\n<pre>cd \/var\/www\r\nmkdir static<\/pre>\n<\/blockquote>\n<p>Check the directory ownership is correct using &#8220;ls -la&#8221;, which should show &#8220;drwxr-xr-x 2 pi pi 4096 Nov 27 23:02 static&#8221;.<\/p>\n<p>If necessary &#8220;chown pi:pi static&#8221;.<\/p>\n<p>Place the HTML + JavaScript file here (I suggest using the scp command, for example &#8220;scp dashboard.html pi@192.168.1.1:\/var\/www\/static&#8221;).<\/p>\n<p><strong>Change the Apache2 config<\/strong><\/p>\n<p>There are two things to change, first to make apache2 listen on port 8001, and secondly to tell it to use the files in \/var\/www\/static in connection with port 8001 requests.<\/p>\n<blockquote>\n<pre>cd \/etc\/apache2\r\nsudo nano ports.conf<\/pre>\n<\/blockquote>\n<p>Add a single line &#8220;Listen 8001&#8221;, then save and exit.<\/p>\n<p>For the second step, enter the &#8220;sites-enabled&#8221; directory. I chose to copy the existing emoncms.conf file, naming the copy &#8220;static.conf&#8221; and editing it to contain:<\/p>\n<blockquote>\n<pre>&lt;VirtualHost *:8001&gt;\r\n  ServerName localhost\r\n  ServerAdmin webmaster@localhost\r\n  DocumentRoot \/var\/www\/static\r\n\r\n  # Virtual Host specific error log\r\n  ErrorLog \/var\/log\/static\/apache2-error.log\r\n\r\n  &lt;Directory \/var\/www\/static&gt;\r\n    Options FollowSymLinks Indexes\r\n    AllowOverride All\r\n    DirectoryIndex index.html\r\n    Order allow,deny\r\n    Allow from all\r\n  &lt;\/Directory&gt;\r\n&lt;\/VirtualHost&gt;<\/pre>\n<\/blockquote>\n<p>The changes are fairly obvious, but note that I have also added &#8220;Indexes&#8221; against &#8220;Options&#8221;. This makes apache give a listing of all the files in &#8220;static&#8221; when I use the URL &#8220;http:\/\/192.168.1.1:8001&#8221;. If you only have one dashboard, then simply name it &#8220;index.html&#8221; and it will appear when that URL is used.<\/p>\n<p><strong>Make a directory for logs<\/strong><\/p>\n<p><em>Otherwise apache2 will not restart.<\/em><\/p>\n<blockquote>\n<pre>cd \/var\/log\r\nmkdir static<\/pre>\n<\/blockquote>\n<p><strong>Restart Apache2<\/strong><\/p>\n<p><em>To load the new config.<\/em><\/p>\n<blockquote>\n<pre>sudo systemctl restart apache2<\/pre>\n<\/blockquote>\n<p>Make sure EmonCMS is still working, then visit your new site! It should work adequately on a mobile phone, once rotated to landscape format.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is not a complete description of the background tech; there is plenty of info on the web about websockets,&#8230;<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[34,35,36,1],"tags":[],"class_list":["post-654","post","type-post","status-publish","format-standard","hentry","category-iot","category-mqtt","category-open-energy-monitor","category-uncategorized","post-archive"],"_links":{"self":[{"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/posts\/654","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/comments?post=654"}],"version-history":[{"count":11,"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/posts\/654\/revisions"}],"predecessor-version":[{"id":665,"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/posts\/654\/revisions\/665"}],"wp:attachment":[{"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/media?parent=654"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/categories?post=654"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.hilltop-cottage.info\/blogs\/adam\/wp-json\/wp\/v2\/tags?post=654"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}