+
@@ -393,6 +400,11 @@
} );
});
+ // Open the format editor
+ $("#format-btn").click(function(e){
+ window.location.href = "/format.htm";
+ });
+
function updateSleepInfo() {
var i = $("#sleep-interval").val()
$("#sleep-interval-info").text( Math.floor(i/60) + " m " + (i%60) + " s" )
@@ -417,6 +429,7 @@
$("#push-btn").prop("disabled", b);
$("#gravity-btn").prop("disabled", b);
$("#hardware-btn").prop("disabled", b);
+ $("#format-btn").prop("disabled", b);
}
// Get the configuration values from the API
diff --git a/html/config.min.htm b/html/config.min.htm
index 5063a45..c1d3d3e 100644
--- a/html/config.min.htm
+++ b/html/config.min.htm
@@ -1 +1 @@
-Beer Gravity Monitor
...
A sleep-interval of less than 300s will reduce battery life, consider using 900s
When using the gyro temperature use a sleep-interval that is greater than 300s for accurate readings
(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file
+Beer Gravity Monitor
...
A sleep-interval of less than 300s will reduce battery life, consider using 900s
When using the gyro temperature use a sleep-interval that is greater than 300s for accurate readings
(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file
diff --git a/html/device.htm b/html/device.htm
index 7dad2be..c39de14 100644
--- a/html/device.htm
+++ b/html/device.htm
@@ -100,7 +100,7 @@
$('#spinner').show();
$.getJSON(url, function (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"]);
$("#id").text(cfg["id"]);
})
diff --git a/html/device.min.htm b/html/device.min.htm
index af7f532..1ac0ad2 100644
--- a/html/device.min.htm
+++ b/html/device.min.htm
@@ -1 +1 @@
-Beer Gravity Monitor
...
Current version:
Loading...
New version:
Loading...
Host name:
Loading...
Device ID:
Loading...
(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file
+Beer Gravity Monitor
...
Current version:
Loading...
New version:
Loading...
Host name:
Loading...
Device ID:
Loading...
(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file
diff --git a/html/format.htm b/html/format.htm
new file mode 100644
index 0000000..ef792b3
--- /dev/null
+++ b/html/format.htm
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+ Beer Gravity Monitor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(C) Copyright 2021-22 Magnus Persson
+
+
\ No newline at end of file
diff --git a/html/format.min.htm b/html/format.min.htm
new file mode 100644
index 0000000..1c58b45
--- /dev/null
+++ b/html/format.min.htm
@@ -0,0 +1,2 @@
+Beer Gravity Monitor
...
(C) Copyright 2021-22 Magnus Persson
\ No newline at end of file
diff --git a/script/copy_html.py b/script/copy_html.py
index 428074b..ad9f861 100644
--- a/script/copy_html.py
+++ b/script/copy_html.py
@@ -26,3 +26,6 @@ shutil.copyfile( source + file, target + file )
file = "upload.min.htm"
#print( "Copy file: " + 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 )
diff --git a/src/helper.cpp b/src/helper.cpp
index 3c1e525..fbb1a1b 100644
--- a/src/helper.cpp
+++ b/src/helper.cpp
@@ -309,4 +309,74 @@ float reduceFloatPrecision(float f, int dec) {
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(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(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
diff --git a/src/helper.hpp b/src/helper.hpp
index bbabaab..81088e8 100644
--- a/src/helper.hpp
+++ b/src/helper.hpp
@@ -39,6 +39,10 @@ double convertToSG(double plato);
float convertCtoF(float c);
float convertFtoC(float f);
+// url encode/decode
+String urldecode(String str);
+String urlencode(String str);
+
// Float to String
char* convertFloatToString(float f, char* buf, int dec = 2);
float reduceFloatPrecision(float f, int dec = 2);
diff --git a/src/main.cpp b/src/main.cpp
index 00f5b35..958fecc 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -235,8 +235,8 @@ bool loopReadGravity() {
float tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
LOG_PERF_STOP("loop-temp-read");
- float gravity = calculateGravity(angle, tempC);
- float corrGravity = gravityTemperatureCorrectionC(gravity, tempC);
+ float gravitySG = calculateGravity(angle, tempC);
+ float corrGravitySG = gravityTemperatureCorrectionC(gravitySG, tempC);
#if LOG_LEVEL == 6 && !defined(MAIN_DISABLE_LOGGING)
Log.verbose(F("Main: Sensor values gyro angle=%F, temp=%FC, gravity=%F, "
@@ -246,7 +246,7 @@ bool loopReadGravity() {
LOG_PERF_START("loop-push");
// Force the transmission if we are going to sleep
- myPushTarget.send(angle, gravity, corrGravity, tempC,
+ myPushTarget.send(angle, gravitySG, corrGravitySG, tempC,
(millis() - runtimeMillis) / 1000,
runMode == RunMode::gravityMode ? true : false);
LOG_PERF_STOP("loop-push");
diff --git a/src/pushtarget.cpp b/src/pushtarget.cpp
index a38a094..cd7cd70 100644
--- a/src/pushtarget.cpp
+++ b/src/pushtarget.cpp
@@ -39,7 +39,7 @@ PushTarget myPushTarget;
//
// 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) {
uint32_t timePassed = abs((int32_t)(millis() - _ms));
uint32_t interval = myConfig.getSleepInterval() * 1000;
@@ -54,35 +54,36 @@ void PushTarget::send(float angle, float gravity, float corrGravity,
_ms = millis();
+ TemplatingEngine engine;
+ engine.initialize(angle, gravitySG, corrGravitySG, tempC, runTime);
+
if (myConfig.isBrewfatherActive()) {
LOG_PERF_START("push-brewfather");
- sendBrewfather(angle, gravity, corrGravity, tempC);
+ sendBrewfather(engine);
LOG_PERF_STOP("push-brewfather");
}
if (myConfig.isHttpActive()) {
LOG_PERF_START("push-http");
- sendHttp(myConfig.getHttpPushUrl(), angle, gravity, corrGravity, tempC,
- runTime);
+ sendHttp(engine, 0);
LOG_PERF_STOP("push-http");
}
if (myConfig.isHttpActive2()) {
LOG_PERF_START("push-http2");
- sendHttp(myConfig.getHttpPushUrl2(), angle, gravity, corrGravity, tempC,
- runTime);
+ sendHttp(engine, 1);
LOG_PERF_STOP("push-http2");
}
if (myConfig.isInfluxDb2Active()) {
LOG_PERF_START("push-influxdb2");
- sendInfluxDb2(angle, gravity, corrGravity, tempC, runTime);
+ sendInfluxDb2(engine);
LOG_PERF_STOP("push-influxdb2");
}
if (myConfig.isMqttActive()) {
LOG_PERF_START("push-mqtt");
- sendMqtt(angle, gravity, corrGravity, tempC, runTime);
+ sendMqtt(engine);
LOG_PERF_STOP("push-mqtt");
}
}
@@ -90,54 +91,29 @@ void PushTarget::send(float angle, float gravity, float corrGravity,
//
// Send to influx db v2
//
-void PushTarget::sendInfluxDb2(float angle, float gravity, float corrGravity,
- float tempC, float runTime) {
+void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
#if !defined(PUSH_DISABLE_LOGGING)
- Log.notice(
- F("PUSH: Sending values to influxdb2 angle=%F, gravity=%F, temp=%F." CR),
- angle, gravity, tempC);
+ Log.notice(F("PUSH: Sending values to influxdb2." CR));
#endif
- HTTPClient http;
String serverPath =
String(myConfig.getInfluxDb2PushUrl()) +
"/api/v2/write?org=" + String(myConfig.getInfluxDb2PushOrg()) +
"&bucket=" + String(myConfig.getInfluxDb2PushBucket());
+ String doc = engine.create(TemplatingEngine::TEMPLATE_INFLUX);
+ HTTPClient http;
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)
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
// Send HTTP POST request
String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
http.addHeader(F("Authorization"), auth.c_str());
- int httpResponseCode = http.POST(&buf[0]);
+ int httpResponseCode = http.POST(doc);
if (httpResponseCode == 204) {
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
//
-void PushTarget::sendBrewfather(float angle, float gravity, float corrGravity,
- float tempC) {
+void PushTarget::sendBrewfather(TemplatingEngine& engine) {
#if !defined(PUSH_DISABLE_LOGGING)
- Log.notice(F("PUSH: Sending values to brewfather angle=%F, gravity=%F, "
- "corr-gravity=%F, temp=%F." CR),
- angle, gravity, corrGravity, tempC);
+ Log.notice(F("PUSH: Sending values to brewfather" CR));
#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 doc = engine.create(TemplatingEngine::TEMPLATE_BREWFATHER);
// Your Domain name with URL path or IP address with path
+ HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath);
- String json;
- serializeJson(doc, json);
+
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
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
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json"));
- int httpResponseCode = http.POST(json);
+ int httpResponseCode = http.POST(doc);
if (httpResponseCode == 200) {
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
//
-void PushTarget::createIspindleFormat(DynamicJsonDocument &doc, float angle,
- 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) {
+void PushTarget::sendHttp(TemplatingEngine& engine, int index) {
#if !defined(PUSH_DISABLE_LOGGING)
- Log.notice(F("PUSH: Sending values to http angle=%F, gravity=%F, "
- "corr-gravity=%F, temp=%F." CR),
- angle, gravity, corrGravity, tempC);
+ Log.notice(F("PUSH: Sending values to http (%s)" CR), index ? "http2" : "http1");
#endif
- DynamicJsonDocument doc(256);
- createIspindleFormat(doc, angle, gravity, corrGravity, tempC, runTime);
+ String serverPath, doc;
+
+ if (index == 0) {
+ serverPath = myConfig.getHttpPushUrl();
+ doc = engine.create(TemplatingEngine::TEMPLATE_HTTP1);
+ }
+ else {
+ serverPath = myConfig.getHttpPushUrl2();
+ doc = engine.create(TemplatingEngine::TEMPLATE_HTTP2);
+ }
HTTPClient http;
-
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
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);
}
- String json;
- serializeJson(doc, json);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
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
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json"));
- int httpResponseCode = http.POST(json);
+ int httpResponseCode = http.POST(doc);
if (httpResponseCode == 200) {
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
//
-void PushTarget::sendMqtt(float angle, float gravity, float corrGravity,
- float tempC, float runTime) {
+void PushTarget::sendMqtt(TemplatingEngine& engine) {
#if !defined(PUSH_DISABLE_LOGGING)
- Log.notice(F("PUSH: Sending values to mqtt angle=%F, gravity=%F, "
- "corr-gravity=%F, temp=%F." CR),
- angle, gravity, corrGravity, tempC);
+ Log.notice(F("PUSH: Sending values to mqtt." CR));
#endif
- DynamicJsonDocument doc(256);
- createIspindleFormat(doc, angle, gravity, corrGravity, tempC, runTime);
-
- MQTTClient mqtt(512); // Maximum message size
+ MQTTClient mqtt(512);
String url = myConfig.getMqttUrl();
+ String doc = engine.create(TemplatingEngine::TEMPLATE_MQTT);
if (url.endsWith(":8883")) {
// 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(),
myConfig.getMqttPass());
- String json;
- serializeJson(doc, json);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
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
// Send MQQT message
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));
} else {
Log.error(F("PUSH: MQTT publish failed err=%d, ret=%d" CR),
diff --git a/src/pushtarget.hpp b/src/pushtarget.hpp
index c77fab6..97683c7 100644
--- a/src/pushtarget.hpp
+++ b/src/pushtarget.hpp
@@ -24,25 +24,20 @@ SOFTWARE.
#ifndef SRC_PUSHTARGET_HPP_
#define SRC_PUSHTARGET_HPP_
+#include
+
class PushTarget {
private:
uint32_t _ms; // Used to check that we do not post to often
- void sendBrewfather(float angle, float gravity, float corrGravity,
- float tempC);
- void sendHttp(String serverPath, float angle, float gravity,
- float corrGravity, float tempC, float runTime);
- 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);
+ void sendBrewfather(TemplatingEngine& engine);
+ void sendHttp(TemplatingEngine& engine, int index);
+ void sendInfluxDb2(TemplatingEngine& engine);
+ void sendMqtt(TemplatingEngine& engine);
public:
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);
};
diff --git a/src/resources.cpp b/src/resources.cpp
index d3de083..66685ec 100644
--- a/src/resources.cpp
+++ b/src/resources.cpp
@@ -34,6 +34,7 @@ INCBIN(IndexHtm, "data/index.min.htm");
INCBIN(DeviceHtm, "data/device.min.htm");
INCBIN(ConfigHtm, "data/config.min.htm");
INCBIN(CalibrationHtm, "data/calibration.min.htm");
+INCBIN(FormatHtm, "data/format.min.htm");
INCBIN(AboutHtm, "data/about.min.htm");
#else
// Minium web interface for uploading htm files
diff --git a/src/resources.hpp b/src/resources.hpp
index add3db0..3136f91 100644
--- a/src/resources.hpp
+++ b/src/resources.hpp
@@ -67,5 +67,10 @@ SOFTWARE.
#define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation"
#define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp"
#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_
diff --git a/src/templating.cpp b/src/templating.cpp
new file mode 100644
index 0000000..a94a53b
--- /dev/null
+++ b/src/templating.cpp
@@ -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
+#include
+
+#if defined (ESP8266)
+#include
+#else // defined (ESP32)
+#include
+#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
diff --git a/src/templating.hpp b/src/templating.hpp
new file mode 100644
index 0000000..0aee648
--- /dev/null
+++ b/src/templating.hpp
@@ -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
+#include
+#include
+#include
+
+// 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
diff --git a/src/webserver.cpp b/src/webserver.cpp
index 6019e20..317b39c 100644
--- a/src/webserver.cpp
+++ b/src/webserver.cpp
@@ -29,6 +29,7 @@ SOFTWARE.
#include
#include
#include
+#include
#include
WebServerHandler myWebServerHandler; // My wrapper class fr webserver functions
@@ -114,6 +115,7 @@ void WebServerHandler::webHandleUpload() {
doc["device"] = checkHtmlFile(WebServerHandler::HTML_DEVICE);
doc["config"] = checkHtmlFile(WebServerHandler::HTML_CONFIG);
doc["calibration"] = checkHtmlFile(WebServerHandler::HTML_CALIBRATION);
+ doc["format"] = checkHtmlFile(WebServerHandler::HTML_FORMAT);
doc["about"] = checkHtmlFile(WebServerHandler::HTML_ABOUT);
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
@@ -522,7 +524,7 @@ void WebServerHandler::webHandleDeviceParam() {
//
void WebServerHandler::webHandleFormulaRead() {
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);
const RawFormulaData& fd = myConfig.getFormulaData();
@@ -583,13 +585,150 @@ void WebServerHandler::webHandleFormulaRead() {
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.
//
void WebServerHandler::webHandleFormulaWrite() {
LOG_PERF_START("webserver-api-formula-write");
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())) {
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";
case HTML_CALIBRATION:
return "calibration.min.htm";
+ case HTML_FORMAT:
+ return "format.min.htm";
case HTML_ABOUT:
return "about.min.htm";
}
@@ -727,6 +868,7 @@ bool WebServerHandler::setupWebServer() {
_server->on("/config.htm", std::bind(&WebServerHandler::webReturnConfigHtm, this));
_server->on("/calibration.htm",
std::bind(&WebServerHandler::webReturnCalibrationHtm, this));
+ _server->on("/format.htm", std::bind(&WebServerHandler::webReturnFormatHtm, this));
_server->on("/about.htm", std::bind(&WebServerHandler::webReturnAboutHtm, this));
#else
// Show files in the filessytem at startup
@@ -745,7 +887,7 @@ bool WebServerHandler::setupWebServer() {
// upload page.
if (checkHtmlFile(HTML_INDEX) && checkHtmlFile(HTML_DEVICE) &&
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));
_server->serveStatic("/", LittleFS, "/index.min.htm");
@@ -754,6 +896,7 @@ bool WebServerHandler::setupWebServer() {
_server->serveStatic("/config.htm", LittleFS, "/config.min.htm");
_server->serveStatic("/about.htm", LittleFS, "/about.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
// be fixed.
@@ -808,6 +951,12 @@ bool WebServerHandler::setupWebServer() {
_server->on("/api/config/hardware", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigHardware,
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,
std::bind(&WebServerHandler::webHandleDeviceParam,
this)); // Change device params
diff --git a/src/webserver.hpp b/src/webserver.hpp
index 49f4b5c..bfd98c3 100644
--- a/src/webserver.hpp
+++ b/src/webserver.hpp
@@ -40,6 +40,7 @@ INCBIN_EXTERN(IndexHtm);
INCBIN_EXTERN(DeviceHtm);
INCBIN_EXTERN(ConfigHtm);
INCBIN_EXTERN(CalibrationHtm);
+INCBIN_EXTERN(FormatHtm);
INCBIN_EXTERN(AboutHtm);
#else
INCBIN_EXTERN(UploadHtm);
@@ -58,6 +59,8 @@ class WebServerHandler {
void webHandleConfigGravity();
void webHandleConfigPush();
void webHandleConfigDevice();
+ void webHandleConfigFormatRead();
+ void webHandleConfigFormatWrite();
void webHandleStatusSleepmode();
void webHandleClearWIFI();
void webHandleStatus();
@@ -69,6 +72,9 @@ class WebServerHandler {
void webHandleDeviceParam();
void webHandlePageNotFound();
+ String readFile(String fname);
+ bool writeFile(String fname, String data);
+
String getRequestArguments();
// Inline functions.
@@ -90,6 +96,10 @@ class WebServerHandler {
_server->send_P(200, "text/html", (const char*)gCalibrationHtmData,
gCalibrationHtmSize);
}
+ void webReturnFormatHtm() {
+ _server->send_P(200, "text/html", (const char*)gFormatHtmData,
+ gFormatHtmSize);
+ }
void webReturnAboutHtm() {
_server->send_P(200, "text/html", (const char*)gAboutHtmData,
gAboutHtmSize);
@@ -107,7 +117,8 @@ class WebServerHandler {
HTML_DEVICE = 1,
HTML_CONFIG = 2,
HTML_ABOUT = 3,
- HTML_CALIBRATION = 4
+ HTML_CALIBRATION = 4,
+ HTML_FORMAT = 5
};
bool setupWebServer();
diff --git a/src_docs/source/configuration.rst b/src_docs/source/configuration.rst
index 01df80c..9157a8b 100644
--- a/src_docs/source/configuration.rst
+++ b/src_docs/source/configuration.rst
@@ -229,6 +229,96 @@ Hardware Settings
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
@@ -590,7 +680,27 @@ This is the format used for standard http posts.
"gravity-unit": "G",
"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:
@@ -621,7 +731,13 @@ This is the format for InfluxDB v2
.. code-block::
measurement,host=,device=,temp-format=,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
============
diff --git a/src_docs/source/images/format.png b/src_docs/source/images/format.png
new file mode 100644
index 0000000..b48d81d
Binary files /dev/null and b/src_docs/source/images/format.png differ
diff --git a/src_docs/source/releases.rst b/src_docs/source/releases.rst
index 2b4b597..705a18e 100644
--- a/src_docs/source/releases.rst
+++ b/src_docs/source/releases.rst
@@ -17,6 +17,9 @@ Development version (dev branch)
the configuration and update the factor again.
* Added error handling for calibration page.
* 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:
Update docs, MQTT ssl is enabled using :8883 at end, http targets enables using prefix https://
diff --git a/test/format.json b/test/format.json
new file mode 100644
index 0000000..459e80d
--- /dev/null
+++ b/test/format.json
@@ -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"
+}
\ No newline at end of file