Added format editor and template engine

This commit is contained in:
Magnus Persson 2022-01-23 11:11:41 +01:00
parent 7b4e95b5ad
commit d3a71da643
23 changed files with 992 additions and 165 deletions

View File

@ -3,6 +3,8 @@ repos:
rev: 9a5aa38207bf557961110d6a4f7e3a9d352911f9 rev: 9a5aa38207bf557961110d6a4f7e3a9d352911f9
hooks: hooks:
- id: clang-format - id: clang-format
files: ^src/
- id: cpplint - id: cpplint
files: ^src/
- id: cppcheck - id: cppcheck
files: ^src/

View File

@ -254,6 +254,13 @@
</div> </div>
</div> </div>
</form> </form>
<div class="form-group row">
<div class="col-sm-8 offset-sm-2">
<button class="btn btn-secondary" id="format-btn">Format editor</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -393,6 +400,11 @@
} ); } );
}); });
// Open the format editor
$("#format-btn").click(function(e){
window.location.href = "/format.htm";
});
function updateSleepInfo() { function updateSleepInfo() {
var i = $("#sleep-interval").val() var i = $("#sleep-interval").val()
$("#sleep-interval-info").text( Math.floor(i/60) + " m " + (i%60) + " s" ) $("#sleep-interval-info").text( Math.floor(i/60) + " m " + (i%60) + " s" )
@ -417,6 +429,7 @@
$("#push-btn").prop("disabled", b); $("#push-btn").prop("disabled", b);
$("#gravity-btn").prop("disabled", b); $("#gravity-btn").prop("disabled", b);
$("#hardware-btn").prop("disabled", b); $("#hardware-btn").prop("disabled", b);
$("#format-btn").prop("disabled", b);
} }
// Get the configuration values from the API // Get the configuration values from the API

File diff suppressed because one or more lines are too long

View File

@ -100,7 +100,7 @@
$('#spinner').show(); $('#spinner').show();
$.getJSON(url, function (cfg) { $.getJSON(url, function (cfg) {
console.log( cfg ); console.log( cfg );
$("#app-ver").text(cfg["app-ver"] + " (html 0.6.0)"); $("#app-ver").text(cfg["app-ver"] + " (html 0.7.0)");
$("#mdns").text(cfg["mdns"]); $("#mdns").text(cfg["mdns"]);
$("#id").text(cfg["id"]); $("#id").text(cfg["id"]);
}) })

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/calibration.htm">Calibration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><hr class="my-4"></div><script type="text/javascript">function getConfig(){var n="/api/device";$("#spinner").show(),$.getJSON(n,function(n){console.log(n),$("#app-ver").text(n["app-ver"]+" (html 0.6.0)"),$("#mdns").text(n.mdns),$("#id").text(n.id)}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html> <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/calibration.htm">Calibration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><hr class="my-4"></div><script type="text/javascript">function getConfig(){var n="/api/device";$("#spinner").show(),$.getJSON(n,function(n){console.log(n),$("#app-ver").text(n["app-ver"]+" (html 0.7.0)"),$("#mdns").text(n.mdns),$("#id").text(n.id)}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html>

224
html/format.htm Normal file
View File

@ -0,0 +1,224 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body class="py-4">
<!-- START MENU -->
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/config.htm">Back to configuration</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</nav>
<!-- START MAIN INDEX -->
<div class="container">
<hr class="my-2">
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert">
<div id="alert-msg">...</div>
<button type="button" id="alert-btn" class="close" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function showError( msg ) {
$('#alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show');
$('#alert-msg').text( msg );
}
function showSuccess( msg ) {
$('#alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show');
$('#alert-msg').text( msg );
}
$("#alert-btn").click(function(e){
$('#alert').addClass('d-none').removeClass('show');
});
</script>
<div class="accordion" id="accordion">
<div class="card">
<div class="card-header" id="headingOne">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Push Format Templates
</button>
</h2>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
<input type="text" name="id" id="id" hidden>
<input type="text" name="http-1" id="http-1" hidden>
<input type="text" name="http-2" id="http-2" hidden>
<!--<input type="text" name="brewfather" id="brewfather" hidden>-->
<input type="text" name="influxdb" id="influxdb" hidden>
<input type="text" name="mqtt" id="mqtt" hidden>
<div class="form-group row">
<label for="push-target" class="col-sm-2 col-form-label">Push target:</label>
<select class="custom-select col-sm-4" required name="push-target" id="push-target">
<option value="http-1">HTTP option 1</option>
<option value="http-2">HTTP option 2</option>
<!--<option value="brewfather">Brewfather</option>-->
<option value="influxdb">Influx DB</option>
<option value="mqtt">MQTT</option>
</select>
</div>
<div class="form-group row">
<div class="col-sm-12">
<textarea rows="5" class="form-control" name="format" id="format">
</textarea>
</div>
</div>
<div class="form-group row">
<div class="col-sm-8 offset-sm-2">
<button class="btn btn-primary" id="format-btn">Save</button>
<button class="btn btn-secondary" id="test-btn">Test</button>
</div>
</div>
<pre class="card-preview" id="preview" name="preview"></pre>
</div>
</div>
</div>
<hr class="my-4">
</div>
<script type="text/javascript">
window.onload = getConfig;
setButtonDisabled( true );
// Opens the targetet according (if URL has #collapseOne to #collapseFour)
$(document).ready(function () {
if(location.hash != null && location.hash != ""){
$('.collapse').removeClass('in');
$(location.hash + '.collapse').collapse('show');
}
});
$("#push-target").change(function(e){
console.log(e)
selectFormat();
});
// Store the format
$("#format-btn").click(function(e) {
var obj = 'id=' + $("#id").val() + '&' + $("#push-target").val() + '=' + encodeURIComponent($("#format").val());
console.log(obj);
$.ajax( {
type: "POST",
url: "/api/config/format",
data: obj,
success: function(result) { showSuccess('Format stored successfully.'); getConfig(); },
error: function(result) { showError('Unable to store format.'); }
} );
});
// Test the calibration
$("#test-btn").click(function(e) {
var doc = $("#format").val();
doc = doc.replaceAll("${mdns}", "gravmon2");
doc = doc.replaceAll("${id}", "e4a344");
doc = doc.replaceAll("${sleep-interval}", "300");
doc = doc.replaceAll("${temp}", "21.1");
doc = doc.replaceAll("${temp-c}", "21.1");
doc = doc.replaceAll("${temp-f}", "51.3");
doc = doc.replaceAll("${temp-unit}", "C");
doc = doc.replaceAll("${battery}", "3.86");
doc = doc.replaceAll("${rssi}", "-76");
doc = doc.replaceAll("${run-time}", "4.32");
doc = doc.replaceAll("${gravity}", "1.044");
doc = doc.replaceAll("${gravity-sg}", "1.044");
doc = doc.replaceAll("${gravity-plato}", "9.5");
doc = doc.replaceAll("${gravity-unit}", "G");
doc = doc.replaceAll("${corr-gravity}", "1.044");
doc = doc.replaceAll("${corr-gravity-sg}", "1.044");
doc = doc.replaceAll("${corr-gravity-plato}", "9.5");
doc = doc.replaceAll("${angle}", "54.5");
doc = doc.replaceAll("${tilt}", "54.5");
// Format in a readable json string.
try {
var json = JSON.parse(doc);
doc = JSON.stringify(json, null, 2);
} catch(e) {
console.log("Not a javascript object!")
}
$("#preview").text(doc);
});
function setButtonDisabled( b ) {
$("#format-btn").prop("disabled", b);
$("#test-btn").prop("disabled", b);
}
function selectFormat() {
var s = "#" + $("#push-target").val()
console.log(s);
$("#format").val(decodeURIComponent($(s).val()));
$("#preview").text("");
}
// Get the configuration values from the API
function getConfig() {
setButtonDisabled( true );
var url = "/api/config/format";
//var url = "/test/format.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#id").val(cfg["id"]);
$("#http-1").val(cfg["http-1"]);
$("#http-2").val(cfg["http-2"]);
//$("#brewfather").val(cfg["brewfather"]);
$("#influxdb").val(cfg["influxdb"]);
$("#mqtt").val(cfg["mqtt"]);
selectFormat();
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
setButtonDisabled( false );
});
}
</script>
<!-- START FOOTER -->
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div>
</body>
</html>

2
html/format.min.htm Normal file
View File

@ -0,0 +1,2 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/config.htm">Back to configuration</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-2"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$("#alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$("#alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$("#alert").addClass("d-none").removeClass("show")})</script><div class="accordion" id="accordion"><div class="card"><div class="card-header" id="headingOne"><h2 class="mb-0"><button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">Push Format Templates</button></h2></div><div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion"><div class="card-body"><input type="text" name="id" id="id" hidden> <input type="text" name="http-1" id="http-1" hidden> <input type="text" name="http-2" id="http-2" hidden> <input type="text" name="brewfather" id="brewfather" hidden> <input type="text" name="influxdb" id="influxdb" hidden> <input type="text" name="mqtt" id="mqtt" hidden><div class="form-group row"><label for="push-target" class="col-sm-2 col-form-label">Push target:</label> <select class="custom-select col-sm-4" required name="push-target" id="push-target"><option value="http-1">HTTP option 1</option><option value="http-2">HTTP option 2</option><option value="brewfather">Brewfather</option><option value="influxdb">Influx DB</option><option value="mqtt">MQTT</option></select></div><div class="form-group row"><div class="col-sm-12"><textarea rows="5" class="form-control" name="format" id="format">
</textarea></div></div><div class="form-group row"><div class="col-sm-8 offset-sm-2"><button class="btn btn-primary" id="format-btn">Save</button> <button class="btn btn-secondary" id="test-btn">Test</button></div></div><pre class="card-preview" id="preview" name="preview"></pre></div></div></div><hr class="my-4"></div><script type="text/javascript">function setButtonDisabled(e){$("#format-btn").prop("disabled",e),$("#test-btn").prop("disabled",e)}function selectFormat(){var e="#"+$("#push-target").val();console.log(e),$("#format").val(decodeURIComponent($(e).val())),$("#preview").text("")}function getConfig(){setButtonDisabled(!0);var e="/api/config/format";$("#spinner").show(),$.getJSON(e,function(e){console.log(e),$("#id").val(e.id),$("#http-1").val(e["http-1"]),$("#http-2").val(e["http-2"]),$("#brewfather").val(e.brewfather),$("#influxdb").val(e.influxdb),$("#mqtt").val(e.mqtt),selectFormat()}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide(),setButtonDisabled(!1)})}window.onload=getConfig,setButtonDisabled(!0),$(document).ready(function(){null!=location.hash&&""!=location.hash&&($(".collapse").removeClass("in"),$(location.hash+".collapse").collapse("show"))}),$("#push-target").change(function(e){console.log(e),selectFormat()}),$("#format-btn").click(function(e){var l="id="+$("#id").val()+"&"+$("#push-target").val()+"="+encodeURIComponent($("#format").val());console.log(l),$.ajax({type:"POST",url:"/api/config/format",data:l,success:function(e){showSuccess("Format stored successfully."),getConfig()},error:function(e){showError("Unable to store format.")}})}),$("#test-btn").click(function(e){var l=$("#format").val();l=l.replaceAll("${mdns}","gravmon2"),l=l.replaceAll("${id}","e4a344"),l=l.replaceAll("${sleep-interval}","300"),l=l.replaceAll("${temp}","21.1"),l=l.replaceAll("${temp-c}","21.1"),l=l.replaceAll("${temp-f}","51.3"),l=l.replaceAll("${temp-unit}","C"),l=l.replaceAll("${battery}","3.86"),l=l.replaceAll("${rssi}","-76"),l=l.replaceAll("${run-time}","4.32"),l=l.replaceAll("${gravity}","1.044"),l=l.replaceAll("${gravity-sg}","1.044"),l=l.replaceAll("${gravity-plato}","9.5"),l=l.replaceAll("${gravity-unit}","G"),l=l.replaceAll("${corr-gravity}","1.044"),l=l.replaceAll("${corr-gravity-sg}","1.044"),l=l.replaceAll("${corr-gravity-plato}","9.5"),l=l.replaceAll("${angle}","54.5"),l=l.replaceAll("${tilt}","54.5");try{var t=JSON.parse(l);l=JSON.stringify(t,null,2)}catch(e){console.log("Not a javascript object!")}$("#preview").text(l)})</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></div></body></html>

View File

@ -26,3 +26,6 @@ shutil.copyfile( source + file, target + file )
file = "upload.min.htm" file = "upload.min.htm"
#print( "Copy file: " + source + file + "->" + target + file) #print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file ) shutil.copyfile( source + file, target + file )
file = "format.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )

View File

@ -309,4 +309,74 @@ float reduceFloatPrecision(float f, int dec) {
return atof(&buffer[0]); return atof(&buffer[0]);
} }
//
// urlencode
//
// https://circuits4you.com/2019/03/21/esp8266-url-encode-decode-example/
//
String urlencode(String str) {
String encodedString = "";
char c;
char code0;
char code1;
for (int i =0; i < static_cast<int>(str.length()); i++) {
c = str.charAt(i);
if (isalnum(c)){
encodedString += c;
} else {
code1 = (c & 0xf) + '0';
if ((c & 0xf) >9) {
code1 = (c & 0xf) - 10 + 'A';
}
c = (c>>4) & 0xf;
code0 = c + '0';
if (c > 9) {
code0 = c - 10 + 'A';
}
encodedString += '%';
encodedString += code0;
encodedString += code1;
}
}
//Log.verbose(F("HELP: encode=%s" CR), encodedString.c_str());
return encodedString;
}
unsigned char h2int(char c) {
if (c >= '0' && c <='9') {
return((unsigned char)c - '0');
}
if (c >= 'a' && c <='f') {
return((unsigned char)c - 'a' + 10);
}
if (c >= 'A' && c <='F') {
return((unsigned char)c - 'A' + 10);
}
return(0);
}
String urldecode(String str) {
String encodedString = "";
char c;
char code0;
char code1;
for (int i = 0; i < static_cast<int>(str.length()); i++){
c = str.charAt(i);
if (c == '%') {
i++;
code0 = str.charAt(i);
i++;
code1 = str.charAt(i);
c = (h2int(code0) << 4) | h2int(code1);
encodedString += c;
} else {
encodedString += c;
}
}
//Log.verbose(F("HELP: decode=%s" CR), encodedString.c_str());
return encodedString;
}
// EOF // EOF

View File

@ -39,6 +39,10 @@ double convertToSG(double plato);
float convertCtoF(float c); float convertCtoF(float c);
float convertFtoC(float f); float convertFtoC(float f);
// url encode/decode
String urldecode(String str);
String urlencode(String str);
// Float to String // Float to String
char* convertFloatToString(float f, char* buf, int dec = 2); char* convertFloatToString(float f, char* buf, int dec = 2);
float reduceFloatPrecision(float f, int dec = 2); float reduceFloatPrecision(float f, int dec = 2);

View File

@ -235,8 +235,8 @@ bool loopReadGravity() {
float tempC = myTempSensor.getTempC(myConfig.isGyroTemp()); float tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
LOG_PERF_STOP("loop-temp-read"); LOG_PERF_STOP("loop-temp-read");
float gravity = calculateGravity(angle, tempC); float gravitySG = calculateGravity(angle, tempC);
float corrGravity = gravityTemperatureCorrectionC(gravity, tempC); float corrGravitySG = gravityTemperatureCorrectionC(gravitySG, tempC);
#if LOG_LEVEL == 6 && !defined(MAIN_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(MAIN_DISABLE_LOGGING)
Log.verbose(F("Main: Sensor values gyro angle=%F, temp=%FC, gravity=%F, " Log.verbose(F("Main: Sensor values gyro angle=%F, temp=%FC, gravity=%F, "
@ -246,7 +246,7 @@ bool loopReadGravity() {
LOG_PERF_START("loop-push"); LOG_PERF_START("loop-push");
// Force the transmission if we are going to sleep // Force the transmission if we are going to sleep
myPushTarget.send(angle, gravity, corrGravity, tempC, myPushTarget.send(angle, gravitySG, corrGravitySG, tempC,
(millis() - runtimeMillis) / 1000, (millis() - runtimeMillis) / 1000,
runMode == RunMode::gravityMode ? true : false); runMode == RunMode::gravityMode ? true : false);
LOG_PERF_STOP("loop-push"); LOG_PERF_STOP("loop-push");

View File

@ -39,7 +39,7 @@ PushTarget myPushTarget;
// //
// Send the data to targets // Send the data to targets
// //
void PushTarget::send(float angle, float gravity, float corrGravity, void PushTarget::send(float angle, float gravitySG, float corrGravitySG,
float tempC, float runTime, bool force) { float tempC, float runTime, bool force) {
uint32_t timePassed = abs((int32_t)(millis() - _ms)); uint32_t timePassed = abs((int32_t)(millis() - _ms));
uint32_t interval = myConfig.getSleepInterval() * 1000; uint32_t interval = myConfig.getSleepInterval() * 1000;
@ -54,35 +54,36 @@ void PushTarget::send(float angle, float gravity, float corrGravity,
_ms = millis(); _ms = millis();
TemplatingEngine engine;
engine.initialize(angle, gravitySG, corrGravitySG, tempC, runTime);
if (myConfig.isBrewfatherActive()) { if (myConfig.isBrewfatherActive()) {
LOG_PERF_START("push-brewfather"); LOG_PERF_START("push-brewfather");
sendBrewfather(angle, gravity, corrGravity, tempC); sendBrewfather(engine);
LOG_PERF_STOP("push-brewfather"); LOG_PERF_STOP("push-brewfather");
} }
if (myConfig.isHttpActive()) { if (myConfig.isHttpActive()) {
LOG_PERF_START("push-http"); LOG_PERF_START("push-http");
sendHttp(myConfig.getHttpPushUrl(), angle, gravity, corrGravity, tempC, sendHttp(engine, 0);
runTime);
LOG_PERF_STOP("push-http"); LOG_PERF_STOP("push-http");
} }
if (myConfig.isHttpActive2()) { if (myConfig.isHttpActive2()) {
LOG_PERF_START("push-http2"); LOG_PERF_START("push-http2");
sendHttp(myConfig.getHttpPushUrl2(), angle, gravity, corrGravity, tempC, sendHttp(engine, 1);
runTime);
LOG_PERF_STOP("push-http2"); LOG_PERF_STOP("push-http2");
} }
if (myConfig.isInfluxDb2Active()) { if (myConfig.isInfluxDb2Active()) {
LOG_PERF_START("push-influxdb2"); LOG_PERF_START("push-influxdb2");
sendInfluxDb2(angle, gravity, corrGravity, tempC, runTime); sendInfluxDb2(engine);
LOG_PERF_STOP("push-influxdb2"); LOG_PERF_STOP("push-influxdb2");
} }
if (myConfig.isMqttActive()) { if (myConfig.isMqttActive()) {
LOG_PERF_START("push-mqtt"); LOG_PERF_START("push-mqtt");
sendMqtt(angle, gravity, corrGravity, tempC, runTime); sendMqtt(engine);
LOG_PERF_STOP("push-mqtt"); LOG_PERF_STOP("push-mqtt");
} }
} }
@ -90,54 +91,29 @@ void PushTarget::send(float angle, float gravity, float corrGravity,
// //
// Send to influx db v2 // Send to influx db v2
// //
void PushTarget::sendInfluxDb2(float angle, float gravity, float corrGravity, void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
float tempC, float runTime) {
#if !defined(PUSH_DISABLE_LOGGING) #if !defined(PUSH_DISABLE_LOGGING)
Log.notice( Log.notice(F("PUSH: Sending values to influxdb2." CR));
F("PUSH: Sending values to influxdb2 angle=%F, gravity=%F, temp=%F." CR),
angle, gravity, tempC);
#endif #endif
HTTPClient http;
String serverPath = String serverPath =
String(myConfig.getInfluxDb2PushUrl()) + String(myConfig.getInfluxDb2PushUrl()) +
"/api/v2/write?org=" + String(myConfig.getInfluxDb2PushOrg()) + "/api/v2/write?org=" + String(myConfig.getInfluxDb2PushOrg()) +
"&bucket=" + String(myConfig.getInfluxDb2PushBucket()); "&bucket=" + String(myConfig.getInfluxDb2PushBucket());
String doc = engine.create(TemplatingEngine::TEMPLATE_INFLUX);
HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath); http.begin(myWifi.getWifiClient(), serverPath);
float temp = myConfig.isTempC() ? tempC : convertCtoF(tempC);
gravity = myConfig.isGravityTempAdj() ? corrGravity : gravity;
// Create body for influxdb2
char buf[1024];
if (myConfig.isGravitySG()) {
snprintf(&buf[0], sizeof(buf),
"measurement,host=%s,device=%s,temp-format=%c,gravity-format=%s "
"gravity=%.4f,corr-gravity=%.4f,angle=%.2f,temp=%.2f,battery=%.2f,"
"rssi=%d\n",
myConfig.getMDNS(), myConfig.getID(), myConfig.getTempFormat(),
"G", gravity, corrGravity, angle, temp,
myBatteryVoltage.getVoltage(), WiFi.RSSI());
} else {
snprintf(&buf[0], sizeof(buf),
"measurement,host=%s,device=%s,temp-format=%c,gravity-format=%s "
"gravity=%.1f,corr-gravity=%.1f,angle=%.2f,temp=%.2f,battery=%.2f,"
"rssi=%d\n",
myConfig.getMDNS(), myConfig.getID(), myConfig.getTempFormat(),
"G", convertToPlato(gravity), convertToPlato(corrGravity), angle,
convertCtoF(temp), myBatteryVoltage.getVoltage(), WiFi.RSSI());
}
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str()); Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: data %s." CR), &buf[0]); Log.verbose(F("PUSH: data %s." CR), doc.c_str());
#endif #endif
// Send HTTP POST request // Send HTTP POST request
String auth = "Token " + String(myConfig.getInfluxDb2PushToken()); String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
http.addHeader(F("Authorization"), auth.c_str()); http.addHeader(F("Authorization"), auth.c_str());
int httpResponseCode = http.POST(&buf[0]); int httpResponseCode = http.POST(doc);
if (httpResponseCode == 204) { if (httpResponseCode == 204) {
Log.notice(F("PUSH: InfluxDB2 push successful, response=%d" CR), Log.notice(F("PUSH: InfluxDB2 push successful, response=%d" CR),
@ -154,62 +130,26 @@ void PushTarget::sendInfluxDb2(float angle, float gravity, float corrGravity,
// //
// Send data to brewfather // Send data to brewfather
// //
void PushTarget::sendBrewfather(float angle, float gravity, float corrGravity, void PushTarget::sendBrewfather(TemplatingEngine& engine) {
float tempC) {
#if !defined(PUSH_DISABLE_LOGGING) #if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to brewfather angle=%F, gravity=%F, " Log.notice(F("PUSH: Sending values to brewfather" CR));
"corr-gravity=%F, temp=%F." CR),
angle, gravity, corrGravity, tempC);
#endif #endif
DynamicJsonDocument doc(300);
//
// {
// "name": "YourDeviceName", // Required field, this will be the ID in
// Brewfather "temp": 20.32, "aux_temp": 15.61, // Fridge Temp
// "ext_temp": 6.51, // Room Temp
// "temp_unit": "C", // C, F, K
// "gravity": 1.042,
// "gravity_unit": "G", // G, P
// "pressure": 10,
// "pressure_unit": "PSI", // PSI, BAR, KPA
// "ph": 4.12,
// "bpm": 123, // Bubbles Per Minute
// "comment": "Hello World",
// "beer": "Pale Ale"
// "battery": 4.98
// }
//
float temp = myConfig.isTempC() ? tempC : convertCtoF(tempC);
doc["name"] = myConfig.getMDNS();
doc["temp"] = reduceFloatPrecision(temp, 1);
doc["temp_unit"] = String(myConfig.getTempFormat());
doc["battery"] = reduceFloatPrecision(myBatteryVoltage.getVoltage(), 2);
if (myConfig.isGravitySG()) {
doc["gravity"] = reduceFloatPrecision(
myConfig.isGravityTempAdj() ? corrGravity : gravity, 4);
} else {
doc["gravity"] = reduceFloatPrecision(
convertToPlato(myConfig.isGravityTempAdj() ? corrGravity : gravity), 1);
}
doc["gravity_unit"] = myConfig.isGravitySG() ? "G" : "P";
HTTPClient http;
String serverPath = myConfig.getBrewfatherPushUrl(); String serverPath = myConfig.getBrewfatherPushUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_BREWFATHER);
// Your Domain name with URL path or IP address with path // Your Domain name with URL path or IP address with path
HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath); http.begin(myWifi.getWifiClient(), serverPath);
String json;
serializeJson(doc, json);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str()); Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), json.c_str()); Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif #endif
// Send HTTP POST request // Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json")); http.addHeader(F("Content-Type"), F("application/json"));
int httpResponseCode = http.POST(json); int httpResponseCode = http.POST(doc);
if (httpResponseCode == 200) { if (httpResponseCode == 200) {
Log.notice(F("PUSH: Brewfather push successful, response=%d" CR), Log.notice(F("PUSH: Brewfather push successful, response=%d" CR),
@ -226,52 +166,23 @@ void PushTarget::sendBrewfather(float angle, float gravity, float corrGravity,
// //
// Send data to http target // Send data to http target
// //
void PushTarget::createIspindleFormat(DynamicJsonDocument &doc, float angle, void PushTarget::sendHttp(TemplatingEngine& engine, int index) {
float gravity, float corrGravity,
float tempC, float runTime) {
float temp = myConfig.isTempC() ? tempC : convertCtoF(tempC);
// Use iSpindle format for compatibility
doc["name"] = myConfig.getMDNS();
doc["ID"] = myConfig.getID();
doc["token"] = "gravitmon";
doc["interval"] = myConfig.getSleepInterval();
doc["temperature"] = reduceFloatPrecision(temp, 1);
doc["temp-units"] = String(myConfig.getTempFormat());
if (myConfig.isGravitySG()) {
doc["gravity"] = reduceFloatPrecision(
myConfig.isGravityTempAdj() ? corrGravity : gravity, 4);
doc["corr-gravity"] = reduceFloatPrecision(corrGravity, 4);
} else {
doc["gravity"] = reduceFloatPrecision(
convertToPlato(myConfig.isGravityTempAdj() ? corrGravity : gravity), 1);
doc["corr-gravity"] = reduceFloatPrecision(convertToPlato(corrGravity), 1);
}
doc["angle"] = reduceFloatPrecision(angle, 2);
doc["battery"] = reduceFloatPrecision(myBatteryVoltage.getVoltage(), 2);
doc["rssi"] = WiFi.RSSI();
// Some additional information
doc["gravity-unit"] = myConfig.isGravitySG() ? "G" : "P";
doc["run-time"] = reduceFloatPrecision(runTime, 2);
}
//
// Send data to http target
//
void PushTarget::sendHttp(String serverPath, float angle, float gravity,
float corrGravity, float tempC, float runTime) {
#if !defined(PUSH_DISABLE_LOGGING) #if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to http angle=%F, gravity=%F, " Log.notice(F("PUSH: Sending values to http (%s)" CR), index ? "http2" : "http1");
"corr-gravity=%F, temp=%F." CR),
angle, gravity, corrGravity, tempC);
#endif #endif
DynamicJsonDocument doc(256); String serverPath, doc;
createIspindleFormat(doc, angle, gravity, corrGravity, tempC, runTime);
if (index == 0) {
serverPath = myConfig.getHttpPushUrl();
doc = engine.create(TemplatingEngine::TEMPLATE_HTTP1);
}
else {
serverPath = myConfig.getHttpPushUrl2();
doc = engine.create(TemplatingEngine::TEMPLATE_HTTP2);
}
HTTPClient http; HTTPClient http;
if (serverPath.startsWith("https://")) { if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure(); myWifi.getWifiClientSecure().setInsecure();
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR)); Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
@ -280,16 +191,14 @@ void PushTarget::sendHttp(String serverPath, float angle, float gravity,
http.begin(myWifi.getWifiClient(), serverPath); http.begin(myWifi.getWifiClient(), serverPath);
} }
String json;
serializeJson(doc, json);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str()); Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), json.c_str()); Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif #endif
// Send HTTP POST request // Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json")); http.addHeader(F("Content-Type"), F("application/json"));
int httpResponseCode = http.POST(json); int httpResponseCode = http.POST(doc);
if (httpResponseCode == 200) { if (httpResponseCode == 200) {
Log.notice(F("PUSH: HTTP push successful, response=%d" CR), Log.notice(F("PUSH: HTTP push successful, response=%d" CR),
@ -305,19 +214,14 @@ void PushTarget::sendHttp(String serverPath, float angle, float gravity,
// //
// Send data to http target // Send data to http target
// //
void PushTarget::sendMqtt(float angle, float gravity, float corrGravity, void PushTarget::sendMqtt(TemplatingEngine& engine) {
float tempC, float runTime) {
#if !defined(PUSH_DISABLE_LOGGING) #if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to mqtt angle=%F, gravity=%F, " Log.notice(F("PUSH: Sending values to mqtt." CR));
"corr-gravity=%F, temp=%F." CR),
angle, gravity, corrGravity, tempC);
#endif #endif
DynamicJsonDocument doc(256); MQTTClient mqtt(512);
createIspindleFormat(doc, angle, gravity, corrGravity, tempC, runTime);
MQTTClient mqtt(512); // Maximum message size
String url = myConfig.getMqttUrl(); String url = myConfig.getMqttUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_MQTT);
if (url.endsWith(":8883")) { if (url.endsWith(":8883")) {
// Allow secure channel, but without certificate validation // Allow secure channel, but without certificate validation
@ -332,16 +236,14 @@ void PushTarget::sendMqtt(float angle, float gravity, float corrGravity,
mqtt.connect(myConfig.getMDNS(), myConfig.getMqttUser(), mqtt.connect(myConfig.getMDNS(), myConfig.getMqttUser(),
myConfig.getMqttPass()); myConfig.getMqttPass());
String json;
serializeJson(doc, json);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), myConfig.getMqttUrl()); Log.verbose(F("PUSH: url %s." CR), myConfig.getMqttUrl());
Log.verbose(F("PUSH: json %s." CR), json.c_str()); Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif #endif
// Send MQQT message // Send MQQT message
mqtt.setTimeout(10); // 10 seconds timeout mqtt.setTimeout(10); // 10 seconds timeout
if (mqtt.publish(myConfig.getMqttTopic(), json)) { if (mqtt.publish(myConfig.getMqttTopic(), doc)) {
Log.notice(F("PUSH: MQTT publish successful" CR)); Log.notice(F("PUSH: MQTT publish successful" CR));
} else { } else {
Log.error(F("PUSH: MQTT publish failed err=%d, ret=%d" CR), Log.error(F("PUSH: MQTT publish failed err=%d, ret=%d" CR),

View File

@ -24,25 +24,20 @@ SOFTWARE.
#ifndef SRC_PUSHTARGET_HPP_ #ifndef SRC_PUSHTARGET_HPP_
#define SRC_PUSHTARGET_HPP_ #define SRC_PUSHTARGET_HPP_
#include <templating.hpp>
class PushTarget { class PushTarget {
private: private:
uint32_t _ms; // Used to check that we do not post to often uint32_t _ms; // Used to check that we do not post to often
void sendBrewfather(float angle, float gravity, float corrGravity, void sendBrewfather(TemplatingEngine& engine);
float tempC); void sendHttp(TemplatingEngine& engine, int index);
void sendHttp(String serverPath, float angle, float gravity, void sendInfluxDb2(TemplatingEngine& engine);
float corrGravity, float tempC, float runTime); void sendMqtt(TemplatingEngine& engine);
void sendInfluxDb2(float angle, float gravity, float corrGravity, float tempC,
float runTime);
void sendMqtt(float angle, float gravity, float corrGravity, float tempC,
float runTime);
void createIspindleFormat(DynamicJsonDocument &doc, float angle,
float gravity, float corrGravity, float tempC,
float runTime);
public: public:
PushTarget() { _ms = millis(); } PushTarget() { _ms = millis(); }
void send(float angle, float gravity, float corrGravity, float temp, void send(float angle, float gravitySG, float corrGravitySG, float tempC,
float runTime, bool force = false); float runTime, bool force = false);
}; };

View File

@ -34,6 +34,7 @@ INCBIN(IndexHtm, "data/index.min.htm");
INCBIN(DeviceHtm, "data/device.min.htm"); INCBIN(DeviceHtm, "data/device.min.htm");
INCBIN(ConfigHtm, "data/config.min.htm"); INCBIN(ConfigHtm, "data/config.min.htm");
INCBIN(CalibrationHtm, "data/calibration.min.htm"); INCBIN(CalibrationHtm, "data/calibration.min.htm");
INCBIN(FormatHtm, "data/format.min.htm");
INCBIN(AboutHtm, "data/about.min.htm"); INCBIN(AboutHtm, "data/about.min.htm");
#else #else
// Minium web interface for uploading htm files // Minium web interface for uploading htm files

View File

@ -67,5 +67,10 @@ SOFTWARE.
#define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation" #define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation"
#define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp" #define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp"
#define PARAM_HW_WIFI_PORTALTIMEOUT "wifi-portaltimeout" #define PARAM_HW_WIFI_PORTALTIMEOUT "wifi-portaltimeout"
#define PARAM_FORMAT_HTTP1 "http-1"
#define PARAM_FORMAT_HTTP2 "http-2"
#define PARAM_FORMAT_BREWFATHER "brewfather"
#define PARAM_FORMAT_INFLUXDB "influxdb"
#define PARAM_FORMAT_MQTT "mqtt"
#endif // SRC_RESOURCES_HPP_ #endif // SRC_RESOURCES_HPP_

173
src/templating.cpp Normal file
View File

@ -0,0 +1,173 @@
/*
MIT License
Copyright (c) 2021-22 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <templating.hpp>
#include <config.hpp>
#if defined (ESP8266)
#include <ESP8266WiFi.h>
#else // defined (ESP32)
#include <WiFi.h>
#endif
// Use iSpindle format for compatibility
const char iSpindleFormat[] PROGMEM =
"{"
"\"name\" : \"gravmon\", "
"\"ID\": \"${id}\", "
"\"token\" : \"gravmon\", "
"\"interval\": ${sleep-interval}, "
"\"temperature\": ${temp}, "
"\"temp-units\": \"${temp-unit}\", "
"\"gravity\": ${gravity}, "
"\"angle\": ${angle}, "
"\"battery\": ${battery}, "
"\"rssi\": ${rssi}, "
"\"corr-gravity\": ${corr-gravity}, "
"\"gravity-unit\": \"${gravity-unit}\", "
"\"run-time\": ${run-time} "
"}";
const char brewfatherFormat[] PROGMEM =
"{"
"\"name\": \"${mdns}\","
"\"temp\": ${temp}, "
"\"aux_temp\": 0, "
"\"ext_temp\": 0, "
"\"temp_unit\": \"${temp-unit}\", "
"\"gravity\": ${gravity}, "
"\"gravity_unit\": \"${gravity-unit}\", "
"\"pressure\": 0, "
"\"pressure_unit\": \"PSI\", "
"\"ph\": 0, "
"\"bpm\": 0, "
"\"comment\": \"\", "
"\"beer\": \"\", "
"\"battery\": ${battery}"
"}";
const char influxDbFormat[] PROGMEM =
"measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-format=${gravity-unit} "
"gravity=${gravity},corr-gravity=${corr-gravity},angle=${angle},temp=${temp},battery=${battery},"
"rssi=${rssi}\n";
//
// Initialize the variables
//
void TemplatingEngine::initialize(float angle, float gravitySG, float corrGravitySG, float tempC, float runTime) {
// Names
setVal(TPL_MDNS, myConfig.getMDNS());
setVal(TPL_ID, myConfig.getID());
// Temperature
if (myConfig.isTempC()) {
setVal(TPL_TEMP, tempC, 1);
} else {
setVal(TPL_TEMP, convertCtoF(tempC), 1);
}
setVal(TPL_TEMP_C, tempC, 1);
setVal(TPL_TEMP_F, convertCtoF(tempC), 1);
setVal(TPL_TEMP_UNITS, myConfig.getTempFormat());
// Battery & Timer
setVal(TPL_BATTERY, myBatteryVoltage.getVoltage());
setVal(TPL_SLEEP_INTERVAL, myConfig.getSleepInterval());
// Performance metrics
setVal(TPL_RUN_TIME, runTime, 1);
setVal(TPL_RSSI, WiFi.RSSI());
// Angle/Tilt
setVal(TPL_TILT, angle);
setVal(TPL_ANGLE, angle);
// Gravity options
if (myConfig.isGravitySG()) {
setVal(TPL_GRAVITY, gravitySG, 4);
setVal(TPL_GRAVITY_CORR, corrGravitySG, 4);
}
else {
setVal(TPL_GRAVITY, convertToPlato(gravitySG), 1);
setVal(TPL_GRAVITY_CORR, convertToPlato(corrGravitySG), 1);
}
setVal(TPL_GRAVITY_G, gravitySG, 4);
setVal(TPL_GRAVITY_P, convertToPlato(gravitySG), 1);
setVal(TPL_GRAVITY_CORR_G, corrGravitySG, 4);
setVal(TPL_GRAVITY_CORR_P, convertToPlato(corrGravitySG), 1);
setVal(TPL_GRAVITY_UNIT, myConfig.getGravityFormat());
#if LOG_DEBUG == 6
dumpAll();
#endif
}
//
// Create the data using defined template.
//
const String& TemplatingEngine::create(TemplatingEngine::Templates idx) {
String fname;
// Load templates from memory
switch (idx) {
case TEMPLATE_HTTP1:
baseTemplate = iSpindleFormat;
fname = TPL_FNAME_HTTP1;
break;
case TEMPLATE_HTTP2:
baseTemplate = iSpindleFormat;
fname = TPL_FNAME_HTTP2;
break;
case TEMPLATE_BREWFATHER:
baseTemplate = brewfatherFormat;
//fname = TPL_FNAME_BREWFATHER;
break;
case TEMPLATE_INFLUX:
baseTemplate = influxDbFormat;
fname = TPL_FNAME_INFLUXDB;
break;
case TEMPLATE_MQTT:
baseTemplate = iSpindleFormat;
fname = TPL_FNAME_MQTT;
break;
}
// TODO: Add code to load templates from disk if they exist.
File file = LittleFS.open(fname, "r");
if (file) {
char buf[file.size()+1];
memset(&buf[0], 0, file.size()+1);
file.readBytes(&buf[0], file.size());
baseTemplate = String(&buf[0]);
file.close();
Log.notice(F("TPL : Template loaded from disk %s." CR), fname.c_str());
}
// Insert data into template.
transform(baseTemplate);
return baseTemplate;
}
// EOF

146
src/templating.hpp Normal file
View File

@ -0,0 +1,146 @@
/*
MIT License
Copyright (c) 2021-22 Magnus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef SRC_TEMPLATING_HPP_
#define SRC_TEMPLATING_HPP_
// Includes
#include <Arduino.h>
#include <main.hpp>
#include <helper.hpp>
#include <algorithm>
// Templating variables
#define TPL_MDNS "${mdns}"
#define TPL_ID "${id}"
#define TPL_SLEEP_INTERVAL "${sleep-interval}"
#define TPL_TEMP "${temp}"
#define TPL_TEMP_C "${temp-c}"
#define TPL_TEMP_F "${temp-f}"
#define TPL_TEMP_UNITS "${temp-unit}" // C or F
#define TPL_BATTERY "${battery}"
#define TPL_RSSI "${rssi}"
#define TPL_RUN_TIME "${run-time}"
#define TPL_ANGLE "${angle}"
#define TPL_TILT "${tilt}" // same as angle
#define TPL_GRAVITY "${gravity}"
#define TPL_GRAVITY_G "${gravity-sg}"
#define TPL_GRAVITY_P "${gravity-plato}"
#define TPL_GRAVITY_CORR "${corr-gravity}"
#define TPL_GRAVITY_CORR_G "${corr-gravity-sg}"
#define TPL_GRAVITY_CORR_P "${corr-gravity-plato}"
#define TPL_GRAVITY_UNIT "${gravity-unit}" // G or P
#define TPL_FNAME_HTTP1 "/http-1.tpl"
#define TPL_FNAME_HTTP2 "/http-2.tpl"
// #define TPL_FNAME_BREWFATHER "/brewfather.tpl"
#define TPL_FNAME_INFLUXDB "/influxdb.tpl"
#define TPL_FNAME_MQTT "/mqtt.tpl"
extern const char iSpindleFormat[] PROGMEM;
extern const char brewfatherFormat[] PROGMEM;
extern const char influxDbFormat[] PROGMEM;
// Classes
class TemplatingEngine {
private:
struct KeyVal {
String key;
String val;
};
KeyVal items[19] = {
{ TPL_MDNS, "" },
{ TPL_ID, "" },
{ TPL_SLEEP_INTERVAL, "" },
{ TPL_TEMP, "" },
{ TPL_TEMP_C, "" },
{ TPL_TEMP_F, "" },
{ TPL_TEMP_UNITS, "" },
{ TPL_BATTERY, "" },
{ TPL_RSSI, "" },
{ TPL_RUN_TIME, "" },
{ TPL_ANGLE, "" },
{ TPL_TILT, "" },
{ TPL_GRAVITY, "" },
{ TPL_GRAVITY_G, "" },
{ TPL_GRAVITY_P, "" },
{ TPL_GRAVITY_CORR, "" },
{ TPL_GRAVITY_CORR_G, "" },
{ TPL_GRAVITY_CORR_P, "" },
{ TPL_GRAVITY_UNIT, "" }
};
char buffer[20];
String baseTemplate;
void setVal(String key, float val, int dec = 2) { String s = convertFloatToString(val, &buffer[0], dec); s.trim(); setVal(key, s); }
void setVal(String key, int val) { setVal(key, String(val)); }
void setVal(String key, char val) { setVal(key, String(val)); }
void setVal(String key, String val) {
int max = sizeof(items)/sizeof(KeyVal);
for (int i = 0; i < max; i++) {
if (items[i].key.equals(key)) {
items[i].val = val;
return;
}
}
Log.error(F("TPL : Key not found %s." CR), key.c_str());
}
void transform(String& s) {
int max = sizeof(items)/sizeof(KeyVal);
for (int i = 0; i < max; i++) {
while (s.indexOf(items[i].key) != -1)
s.replace(items[i].key, items[i].val);
}
}
void dumpAll() {
int max = sizeof(items)/sizeof(KeyVal);
for (int i = 0; i < max; i++) {
Serial.print( "Key=\'" );
Serial.print( items[i].key.c_str() );
Serial.print( "\', Val=\'" );
Serial.print( items[i].val.c_str() );
Serial.println( "\'" );
}
}
public:
enum Templates {
TEMPLATE_HTTP1 = 0,
TEMPLATE_HTTP2 = 1,
TEMPLATE_BREWFATHER = 2,
TEMPLATE_INFLUX = 3,
TEMPLATE_MQTT = 4
};
void initialize(float angle, float gravitySG, float corrGravitySG, float tempC, float runTime);
const String& create(TemplatingEngine::Templates idx);
};
#endif // SRC_TEMPLATING_HPP_
// EOF

View File

@ -29,6 +29,7 @@ SOFTWARE.
#include <resources.hpp> #include <resources.hpp>
#include <tempsensor.hpp> #include <tempsensor.hpp>
#include <webserver.hpp> #include <webserver.hpp>
#include <templating.hpp>
#include <wifi.hpp> #include <wifi.hpp>
WebServerHandler myWebServerHandler; // My wrapper class fr webserver functions WebServerHandler myWebServerHandler; // My wrapper class fr webserver functions
@ -114,6 +115,7 @@ void WebServerHandler::webHandleUpload() {
doc["device"] = checkHtmlFile(WebServerHandler::HTML_DEVICE); doc["device"] = checkHtmlFile(WebServerHandler::HTML_DEVICE);
doc["config"] = checkHtmlFile(WebServerHandler::HTML_CONFIG); doc["config"] = checkHtmlFile(WebServerHandler::HTML_CONFIG);
doc["calibration"] = checkHtmlFile(WebServerHandler::HTML_CALIBRATION); doc["calibration"] = checkHtmlFile(WebServerHandler::HTML_CALIBRATION);
doc["format"] = checkHtmlFile(WebServerHandler::HTML_FORMAT);
doc["about"] = checkHtmlFile(WebServerHandler::HTML_ABOUT); doc["about"] = checkHtmlFile(WebServerHandler::HTML_ABOUT);
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
@ -522,7 +524,7 @@ void WebServerHandler::webHandleDeviceParam() {
// //
void WebServerHandler::webHandleFormulaRead() { void WebServerHandler::webHandleFormulaRead() {
LOG_PERF_START("webserver-api-formula-read"); LOG_PERF_START("webserver-api-formula-read");
Log.notice(F("WEB : webServer callback for /api/formula/get." CR)); Log.notice(F("WEB : webServer callback for /api/formula(get)." CR));
DynamicJsonDocument doc(250); DynamicJsonDocument doc(250);
const RawFormulaData& fd = myConfig.getFormulaData(); const RawFormulaData& fd = myConfig.getFormulaData();
@ -583,13 +585,150 @@ void WebServerHandler::webHandleFormulaRead() {
LOG_PERF_STOP("webserver-api-formula-read"); LOG_PERF_STOP("webserver-api-formula-read");
} }
//
// Update format template
//
void WebServerHandler::webHandleConfigFormatWrite() {
LOG_PERF_START("webserver-api-config-format-write");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/format(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
myConfig.getID());
_server->send(400, "text/plain", "Invalid ID.");
LOG_PERF_STOP("webserver-api-config-format-write");
return;
}
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
bool success = false;
// Only one option is posted so we done need to check them all.
if (_server->hasArg(PARAM_FORMAT_HTTP1)) {
success = writeFile(TPL_FNAME_HTTP1, _server->arg(PARAM_FORMAT_HTTP1));
} else if (_server->hasArg(PARAM_FORMAT_HTTP2)) {
success = writeFile(TPL_FNAME_HTTP2, _server->arg(PARAM_FORMAT_HTTP2));
} else if (_server->hasArg(PARAM_FORMAT_INFLUXDB)) {
success = writeFile(TPL_FNAME_INFLUXDB, _server->arg(PARAM_FORMAT_INFLUXDB));
} else if (_server->hasArg(PARAM_FORMAT_MQTT)) {
success = writeFile(TPL_FNAME_MQTT, _server->arg(PARAM_FORMAT_MQTT));
}
/*else if (_server->hasArg(PARAM_FORMAT_BREWFATHER)) {
success = writeFile(TPL_FNAME_BREWFATHER, _server->arg(PARAM_FORMAT_BREWFATHER));
}*/
if (success) {
_server->sendHeader("Location", "/format.htm", true);
_server->send(302, "text/plain", "Format updated");
} else {
Log.error(F("WEB : Unable to store format file" CR));
_server->send(400, "text/plain", "Unable to store format in file.");
}
LOG_PERF_STOP("webserver-api-config-format-write");
}
//
// Write file to disk, if there is no data then delete the current file (if it exists) = reset to default.
//
bool WebServerHandler::writeFile(String fname, String data) {
if (data.length()) {
data = urldecode(data);
File file = LittleFS.open(fname, "w");
if (file) {
Log.notice(F("WEB : Storing template data in %s." CR), fname.c_str());
file.write(data.c_str());
file.close();
return true;
}
} else {
Log.notice(F("WEB : No template data to store in %s, reverting to default." CR), fname.c_str());
LittleFS.remove(fname);
return true;
}
return false;
}
//
// Read file from disk
//
String WebServerHandler::readFile(String fname) {
File file = LittleFS.open(fname, "r");
if (file) {
char buf[file.size()+1];
memset(&buf[0], 0, file.size()+1);
file.readBytes(&buf[0], file.size());
file.close();
Log.notice(F("WEB : Read template data from %s." CR), fname.c_str());
return String(&buf[0]);
}
return "";
}
//
// Get format templates
//
void WebServerHandler::webHandleConfigFormatRead() {
LOG_PERF_START("webserver-api-config-format-read");
Log.notice(F("WEB : webServer callback for /api/config/formula(get)." CR));
DynamicJsonDocument doc(2048);
doc[PARAM_ID] = myConfig.getID();
String s = readFile(TPL_FNAME_HTTP1);
if (s.length())
doc[PARAM_FORMAT_HTTP1] = urlencode(s);
else
doc[PARAM_FORMAT_HTTP1] = urlencode(&iSpindleFormat[0]);
s = readFile(TPL_FNAME_HTTP2);
if (s.length())
doc[PARAM_FORMAT_HTTP2] = urlencode(s);
else
doc[PARAM_FORMAT_HTTP2] = urlencode(&iSpindleFormat[0]);
/*s = readFile(TPL_FNAME_BREWFATHER);
if (s.length())
doc[PARAM_FORMAT_BREWFATHER] = urlencode(s);
else
doc[PARAM_FORMAT_BREWFATHER] = urlencode(&brewfatherFormat[0]);*/
s = readFile(TPL_FNAME_INFLUXDB);
if (s.length())
doc[PARAM_FORMAT_INFLUXDB] = urlencode(s);
else
doc[PARAM_FORMAT_INFLUXDB] = urlencode(&influxDbFormat[0]);
s = readFile(TPL_FNAME_MQTT);
if (s.length())
doc[PARAM_FORMAT_MQTT] = urlencode(s);
else
doc[PARAM_FORMAT_MQTT] = urlencode(&iSpindleFormat[0]);
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
Serial.print(CR);
#endif
String out;
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-config-format-read");
}
// //
// Update hardware settings. // Update hardware settings.
// //
void WebServerHandler::webHandleFormulaWrite() { void WebServerHandler::webHandleFormulaWrite() {
LOG_PERF_START("webserver-api-formula-write"); LOG_PERF_START("webserver-api-formula-write");
String id = _server->arg(PARAM_ID); String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/formula/post." CR)); Log.notice(F("WEB : webServer callback for /api/formula(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) { if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -677,6 +816,8 @@ const char* WebServerHandler::getHtmlFileName(HtmlFile item) {
return "config.min.htm"; return "config.min.htm";
case HTML_CALIBRATION: case HTML_CALIBRATION:
return "calibration.min.htm"; return "calibration.min.htm";
case HTML_FORMAT:
return "format.min.htm";
case HTML_ABOUT: case HTML_ABOUT:
return "about.min.htm"; return "about.min.htm";
} }
@ -727,6 +868,7 @@ bool WebServerHandler::setupWebServer() {
_server->on("/config.htm", std::bind(&WebServerHandler::webReturnConfigHtm, this)); _server->on("/config.htm", std::bind(&WebServerHandler::webReturnConfigHtm, this));
_server->on("/calibration.htm", _server->on("/calibration.htm",
std::bind(&WebServerHandler::webReturnCalibrationHtm, this)); std::bind(&WebServerHandler::webReturnCalibrationHtm, this));
_server->on("/format.htm", std::bind(&WebServerHandler::webReturnFormatHtm, this));
_server->on("/about.htm", std::bind(&WebServerHandler::webReturnAboutHtm, this)); _server->on("/about.htm", std::bind(&WebServerHandler::webReturnAboutHtm, this));
#else #else
// Show files in the filessytem at startup // Show files in the filessytem at startup
@ -745,7 +887,7 @@ bool WebServerHandler::setupWebServer() {
// upload page. // upload page.
if (checkHtmlFile(HTML_INDEX) && checkHtmlFile(HTML_DEVICE) && if (checkHtmlFile(HTML_INDEX) && checkHtmlFile(HTML_DEVICE) &&
checkHtmlFile(HTML_CONFIG) && checkHtmlFile(HTML_CALIBRATION) && checkHtmlFile(HTML_CONFIG) && checkHtmlFile(HTML_CALIBRATION) &&
checkHtmlFile(HTML_ABOUT)) { checkHtmlFile(HTML_FORMAT) && checkHtmlFile(HTML_ABOUT)) {
Log.notice(F("WEB : All html files exist, starting in normal mode." CR)); Log.notice(F("WEB : All html files exist, starting in normal mode." CR));
_server->serveStatic("/", LittleFS, "/index.min.htm"); _server->serveStatic("/", LittleFS, "/index.min.htm");
@ -754,6 +896,7 @@ bool WebServerHandler::setupWebServer() {
_server->serveStatic("/config.htm", LittleFS, "/config.min.htm"); _server->serveStatic("/config.htm", LittleFS, "/config.min.htm");
_server->serveStatic("/about.htm", LittleFS, "/about.min.htm"); _server->serveStatic("/about.htm", LittleFS, "/about.min.htm");
_server->serveStatic("/calibration.htm", LittleFS, "/calibration.min.htm"); _server->serveStatic("/calibration.htm", LittleFS, "/calibration.min.htm");
_server->serveStatic("/format.htm", LittleFS, "/format.min.htm");
// Also add the static upload view in case we we have issues that needs to // Also add the static upload view in case we we have issues that needs to
// be fixed. // be fixed.
@ -808,6 +951,12 @@ bool WebServerHandler::setupWebServer() {
_server->on("/api/config/hardware", HTTP_POST, _server->on("/api/config/hardware", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigHardware, std::bind(&WebServerHandler::webHandleConfigHardware,
this)); // Change hardware settings this)); // Change hardware settings
_server->on("/api/config/format", HTTP_GET,
std::bind(&WebServerHandler::webHandleConfigFormatRead,
this)); // Change template formats
_server->on("/api/config/format", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigFormatWrite,
this)); // Change template formats
_server->on("/api/device/param", HTTP_GET, _server->on("/api/device/param", HTTP_GET,
std::bind(&WebServerHandler::webHandleDeviceParam, std::bind(&WebServerHandler::webHandleDeviceParam,
this)); // Change device params this)); // Change device params

View File

@ -40,6 +40,7 @@ INCBIN_EXTERN(IndexHtm);
INCBIN_EXTERN(DeviceHtm); INCBIN_EXTERN(DeviceHtm);
INCBIN_EXTERN(ConfigHtm); INCBIN_EXTERN(ConfigHtm);
INCBIN_EXTERN(CalibrationHtm); INCBIN_EXTERN(CalibrationHtm);
INCBIN_EXTERN(FormatHtm);
INCBIN_EXTERN(AboutHtm); INCBIN_EXTERN(AboutHtm);
#else #else
INCBIN_EXTERN(UploadHtm); INCBIN_EXTERN(UploadHtm);
@ -58,6 +59,8 @@ class WebServerHandler {
void webHandleConfigGravity(); void webHandleConfigGravity();
void webHandleConfigPush(); void webHandleConfigPush();
void webHandleConfigDevice(); void webHandleConfigDevice();
void webHandleConfigFormatRead();
void webHandleConfigFormatWrite();
void webHandleStatusSleepmode(); void webHandleStatusSleepmode();
void webHandleClearWIFI(); void webHandleClearWIFI();
void webHandleStatus(); void webHandleStatus();
@ -69,6 +72,9 @@ class WebServerHandler {
void webHandleDeviceParam(); void webHandleDeviceParam();
void webHandlePageNotFound(); void webHandlePageNotFound();
String readFile(String fname);
bool writeFile(String fname, String data);
String getRequestArguments(); String getRequestArguments();
// Inline functions. // Inline functions.
@ -90,6 +96,10 @@ class WebServerHandler {
_server->send_P(200, "text/html", (const char*)gCalibrationHtmData, _server->send_P(200, "text/html", (const char*)gCalibrationHtmData,
gCalibrationHtmSize); gCalibrationHtmSize);
} }
void webReturnFormatHtm() {
_server->send_P(200, "text/html", (const char*)gFormatHtmData,
gFormatHtmSize);
}
void webReturnAboutHtm() { void webReturnAboutHtm() {
_server->send_P(200, "text/html", (const char*)gAboutHtmData, _server->send_P(200, "text/html", (const char*)gAboutHtmData,
gAboutHtmSize); gAboutHtmSize);
@ -107,7 +117,8 @@ class WebServerHandler {
HTML_DEVICE = 1, HTML_DEVICE = 1,
HTML_CONFIG = 2, HTML_CONFIG = 2,
HTML_ABOUT = 3, HTML_ABOUT = 3,
HTML_CALIBRATION = 4 HTML_CALIBRATION = 4,
HTML_FORMAT = 5
}; };
bool setupWebServer(); bool setupWebServer();

View File

@ -229,6 +229,96 @@ Hardware Settings
http://192.168.1.1/firmware/gravmon/ http://192.168.1.1/firmware/gravmon/
.. _format-editor:
Format editor
#############
To reduce the need for adding custom endpoints for various services there is an built in format editor that allows the user to customize the format being sent to the push target.
.. warning::
Since the format templates can be big this function can be quite slow on a small device such as the esp8266.
.. image:: images/format.png
:width: 800
:alt: Format editor
You enter the format data in the text field and the test button will show an example on what the output would look like. If the data cannot be formatted in json it will just be displayed as a long string.
The save button will save the current formla and reload the data from the device.
.. tip::
If you save a blank string the default template will be loaded.
These are the format keys available for use in the format.
.. list-table:: Directory structure
:widths: 30 50 20
:header-rows: 1
* - key
- description
- example
* - ${mdns}
- Name of the device
- gravmon2
* - ${id}
- Unique id of the device
- e422a3
* - ${sleep-interval}
- Seconds between data is pushed
- 900
* - ${temp}
- Temperature in format configured on device, one decimal
- 21.2
* - ${temp-c}
- Temperature in C, one decimal
- 21.2
* - ${temp-f}
- Temperature in F, one decimal
- 58.0
* - ${temp-unit}
- Temperature format `C` or `F`
- C
* - ${battery}
- Battery voltage, two decimals
- 3.89
* - ${rssi}
- Wifi signal strength
- -75
* - ${run-time}
- How long the last measurement took, two decimals
- 3.87
* - ${angle}
- Angle of the gyro, two decimals
- 28.67
* - ${tilt}
- Same as angle.
- 28.67
* - ${gravity}
- Calculated gravity, 4 decimals for SG and 1 for Plato.
- 1.0456
* - ${gravity-sg}
- Calculated gravity in SG, 4 decimals
- 1.0456
* - ${gravity-plato}
- Calculated gravity in Plato, 1 decimal
- 8.5
* - ${corr-gravity}
- Temperature corrected gravity, 4 decimals for SG and 1 for Plato.
- 1.0456
* - ${corr-gravity-sg}
- Temperature corrected gravity in SG, 4 decimals
- 1.0456
* - ${corr-gravity-plato}
- Temperature corrected gravity in Plato, 1 decimal
- 8.5
* - ${gravity-unit}
- Gravity format, `G` or `P`
- G
.. _create-formula: .. _create-formula:
Create formula Create formula
@ -591,6 +681,26 @@ This is the format used for standard http posts.
"run-time": 6 "run-time": 6
} }
This is the format template used to create the json above.
.. code-block::
{
"name" : "gravmon", "
"ID": "${id}", "
"token" : "gravmon", "
"interval": ${sleep-interval}, "
"temperature": ${temp}, "
"temp-units": "${temp-unit}", "
"gravity": ${gravity}, "
"angle": ${angle}, "
"battery": ${battery}, "
"rssi": ${rssi}, "
"corr-gravity": ${corr-gravity}, "
"gravity-unit": "${gravity-unit}", "
"run-time": ${run-time} "
}
.. _data-formats-brewfather: .. _data-formats-brewfather:
@ -623,6 +733,12 @@ This is the format for InfluxDB v2
measurement,host=<mdns>,device=<id>,temp-format=<C|F>,gravity-format=SG,gravity=1.0004,corr-gravity=1.0004,angle=45.45,temp=20.1,battery=3.96,rssi=-18 measurement,host=<mdns>,device=<id>,temp-format=<C|F>,gravity-format=SG,gravity=1.0004,corr-gravity=1.0004,angle=45.45,temp=20.1,battery=3.96,rssi=-18
This is the format template used to create the json above.
.. code-block::
measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-format=${gravity-unit} gravity=${gravity},corr-gravity=${corr-gravity},angle=${angle},temp=${temp},battery=${battery},rssi=${rssi}
version.json version.json
============ ============

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -17,6 +17,9 @@ Development version (dev branch)
the configuration and update the factor again. the configuration and update the factor again.
* Added error handling for calibration page. * Added error handling for calibration page.
* Added experimental target ESP32 (using an ESP32 D1 Mini which is pin compatible with ESP8266) * Added experimental target ESP32 (using an ESP32 D1 Mini which is pin compatible with ESP8266)
* Added experimental format editor so users can customize their data format used for pushing data.
This will reduce the need for custom push targets. As long as the service is supporting http
or https then the data format can be customized.
TODO: TODO:
Update docs, MQTT ssl is enabled using :8883 at end, http targets enables using prefix https:// Update docs, MQTT ssl is enabled using :8883 at end, http targets enables using prefix https://

8
test/format.json Normal file
View File

@ -0,0 +1,8 @@
{
"id": "7376ef",
"http-1": "%7B%22name%22%20%3A%20%22gravmon%22%2C%20%22ID%22%3A%20%22%24%7Bid%7D%22%2C%20%22token%22%20%3A%20%22gravmon%22%2C%20%22interval%22%3A%20%24%7Bsleep%2Dinterval%7D%2C%20%22temperature%22%3A%20%24%7Btemp%7D%2C%20%22temp%2Dunits%22%3A%20%22%24%7Btemp%2Dunit%7D%22%2C%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%20%22angle%22%3A%20%24%7Bangle%7D%2C%20%22battery%22%3A%20%24%7Bbattery%7D%2C%20%22rssi%22%3A%20%24%7Brssi%7D%2C%20%22corr%2Dgravity%22%3A%20%24%7Bcorr%2Dgravity%7D%2C%20%22gravity%2Dunit%22%3A%20%22%24%7Bgravity%2Dunit%7D%22%2C%20%22run%2Dtime%22%3A%20%24%7Brun%2Dtime%7D%20%7D",
"http-2": "%7B%22name%22%20%3A%20%22gravmon%22%2C%20%22ID%22%3A%20%22%24%7Bid%7D%22%2C%20%22token%22%20%3A%20%22gravmon%22%2C%20%22interval%22%3A%20%24%7Bsleep%2Dinterval%7D%2C%20%22temperature%22%3A%20%24%7Btemp%7D%2C%20%22temp%2Dunits%22%3A%20%22%24%7Btemp%2Dunit%7D%22%2C%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%20%22angle%22%3A%20%24%7Bangle%7D%2C%20%22battery%22%3A%20%24%7Bbattery%7D%2C%20%22rssi%22%3A%20%24%7Brssi%7D%2C%20%22corr%2Dgravity%22%3A%20%24%7Bcorr%2Dgravity%7D%2C%20%22gravity%2Dunit%22%3A%20%22%24%7Bgravity%2Dunit%7D%22%2C%20%22run%2Dtime%22%3A%20%24%7Brun%2Dtime%7D%20%7D",
"brewfather": "%7B%22name%22%3A%20%22%24%7Bmdns%7D%22%2C%22temp%22%3A%20%24%7Btemp%7D%2C%20%22aux%5Ftemp%22%3A%200%2C%20%22ext%5Ftemp%22%3A%200%2C%20%22temp%5Funit%22%3A%20%22%24%7Btemp%2Dunit%7D%22%2C%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%20%22gravity%5Funit%22%3A%20%22%24%7Bgravity%2Dunit%7D%22%2C%20%22pressure%22%3A%200%2C%20%22pressure%5Funit%22%3A%20%22PSI%22%2C%20%22ph%22%3A%200%2C%20%22bpm%22%3A%200%2C%20%22comment%22%3A%20%22%22%2C%20%22beer%22%3A%20%22%22%2C%20%22battery%22%3A%20%24%7Bbattery%7D%7D",
"influxdb": "measurement%2Chost%3D%24%7Bmdns%7D%2Cdevice%3D%24%7Bid%7D%2Ctemp%2Dformat%3D%24%7Btemp%2Dunit%7D%2Cgravity%2Dformat%3D%24%7Bgravity%2Dunit%7D%20gravity%3D%24%7Bgravity%7D%2Ccorr%2Dgravity%3D%24%7Bcorr%2Dgravity%7D%2Cangle%3D%24%7Bangle%7D%2Ctemp%3D%24%7Btemp%7D%2Cbattery%3D%24%7Bbattery%7D%2Crssi%3D%24%7Brssi%7D%0A",
"mqtt": "%7B%22name%22%20%3A%20%22gravmon%22%2C%20%22ID%22%3A%20%22%24%7Bid%7D%22%2C%20%22token%22%20%3A%20%22gravmon%22%2C%20%22interval%22%3A%20%24%7Bsleep%2Dinterval%7D%2C%20%22temperature%22%3A%20%24%7Btemp%7D%2C%20%22temp%2Dunits%22%3A%20%22%24%7Btemp%2Dunit%7D%22%2C%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%20%22angle%22%3A%20%24%7Bangle%7D%2C%20%22battery%22%3A%20%24%7Bbattery%7D%2C%20%22rssi%22%3A%20%24%7Brssi%7D%2C%20%22corr%2Dgravity%22%3A%20%24%7Bcorr%2Dgravity%7D%2C%20%22gravity%2Dunit%22%3A%20%22%24%7Bgravity%2Dunit%7D%22%2C%20%22run%2Dtime%22%3A%20%24%7Brun%2Dtime%7D%20%7D"
}