#include <WiFi.h>
#include <AsyncTCP.h>
#include <AsyncWebServer_ESP32_ENC.h>
#include <Preferences.h>
const bool TEST = false;
const int intervalInSecs = 60;
const char* ssid = "SSID";
const char* password = "PASSWORD";
const char* host = "maker.ifttt.com";
const char* apiKey = "API_KEY";
Preferences preferences;
// https://www.acetank.com/wp-content/uploads/2014/03/Oval-110-135-165-220-275.pdf
const int depthGalMap[45] = { 0, 1, 4, 7, 11, 15, 20, 25, 30, 36, 41, 47, 52, 58, 64, 69, 75,
81, 87, 92, 98, 104, 110, 115, 121, 127, 133, 138, 144, 150, 155,
161, 167, 173, 178, 184, 189, 194, 199, 204, 208, 212, 215, 218, 219 };
// History is a moving window of the past 30 days
const int HISTORY_CAP = 30;
String history[HISTORY_CAP];
int historyOldest = 0;
int historyLatest = 0;
int historyCount = 0;
void pushHistory(String item)
{
if (historyCount < HISTORY_CAP)
{
historyLatest = historyCount;
historyCount++;
}
else
{
historyLatest = historyOldest;
historyOldest = (historyOldest + 1) % HISTORY_CAP;
}
history[historyLatest] = item;
String save;
for (int i = 0; i < historyCount; i++)
{
save += history[i];
}
// Persist
preferences.putString("history", save);
}
void clearHistory()
{
for (int i = 0; i < HISTORY_CAP; i++)
{
history[i] = "";
}
preferences.putString("history", "");
}
AsyncWebServer server(80);
const char* PARAM_INPUT_1 = "input1";
const int trigPin = 5;
const int echoPin = 18;
//define sound speed in cm/uS
#define SOUND_SPEED 0.034
#define CM_TO_INCH 0.393701
long duration;
float distanceCm;
float distanceInch;
float fillInches;
float fillGallons;
float threshold = 10.00;
String dataBuf;
String dataLabelBuf;
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;
const int daylightOffset_sec = 3600;
// Main HTML web page
char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
<script src="https://code.highcharts.com/highcharts.js"></script>
<title>Heating Oil Alerts</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head><body>
220 gallon tank<br>
Current: <b>%CURRENT_FILL% Gallons</b><br>
<form action="/get">
Alert When Below: <input type="number" min="0" max="40", step=".01" name="input1" value="%CURRENT_THRESH%">
<input type="submit" value="Submit">
</form><br>
<div id="chart-daily-usage" class="container"></div>
<script>
var chartT = new Highcharts.Chart({
chart:{ renderTo : 'chart-daily-usage' },
title: { text: 'Daily Usage' },
series: [{
showInLegend: false,
data: [%DATA%]
}],
plotOptions: {
series: { dataLabels: { enabled: true } }
},
xAxis: { categories: [%DATALABELS%] },
yAxis: { title: { text: 'Gallons Used' } },
credits: { enabled: false }
});
</script>
</body></html>)rawliteral";
// Parse strings in history, making 2 comma-separated
// strings for html chart data & labels
void DailyHistory()
{
dataBuf = "";
dataLabelBuf = "";
for (int i = 1; i < historyCount; i++)
{
int offset = (historyOldest + i) % HISTORY_CAP;
dataLabelBuf += "'";
dataLabelBuf += history[offset].substring(0, 8);
dataLabelBuf += "'";
float val = history[offset].substring(8).toFloat() - history[offset-1].substring(8).toFloat();
// Don't log usage for fill days. If the dif is positive, call it zero used.
val = min(val, 0.00);
char diffBuf[4];
dtostrf(val, 4, 2, diffBuf);
dataBuf += diffBuf;
if (i + 1 < historyCount)
{
dataLabelBuf += ',';
dataBuf += ',';
}
}
// Serial.print("Data: ");
// Serial.println(dataBuf);
// Serial.print("Labels: ");
// Serial.println(dataLabelBuf);
}
// html text substitutions
String processor(const String& var)
{
if(var == "CURRENT_FILL"){
return String(fillGallons);
}
else if(var == "CURRENT_THRESH"){
return String(threshold);
}
else if(var == "DATA"){
return dataBuf;
}
else if(var == "DATALABELS"){
return dataLabelBuf;
}
return String();
}
void notFound(AsyncWebServerRequest *request)
{
request->send(404, "text/plain", "Not found");
}
void setup()
{
// Serial port for debugging purposes
Serial.begin(115200);
pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
pinMode(echoPin, INPUT); // Sets the echoPin as an Input
// persistence for chart
preferences.begin("oil-monitor", false);
String save = preferences.getString("history", "");
// parse saved history
String el = save.substring(0, 13);
int index = 0;
while (el != 0)
{
pushHistory(el);
index += 13;
el = save.substring(index, index + 13);
}
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
configTime(0, 0, ntpServer);
// Send web page with input fields to client
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
// Send a GET request to <ESP_IP>/get?input1=<inputMessage>
server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
String inputMessage;
String inputParam;
// GET threshold value <URL>/get?input1=<inputMessage>
if (request->hasParam(PARAM_INPUT_1)) {
inputMessage = request->getParam(PARAM_INPUT_1)->value();
inputParam = PARAM_INPUT_1;
threshold = inputParam.toFloat();
}
else {
inputMessage = "No message sent";
inputParam = "none";
}
Serial.println(inputMessage);
request->send(200, "text/html", "Set minimum gallons threshold to: " + inputMessage +
"<br><a href=\"/\">Return</a>");
});
server.onNotFound(notFound);
server.begin();
}
// Linear interpolation
float lerp(float a, float b, float x)
{
return a + x * (b - a);
}
// Table look up. Inches==index.
float inchesToGallons(float inches)
{
int inchIndex = inches;
if (inchIndex <= 0)
{
return depthGalMap[0];
}
if (inchIndex >= 44)
{
return depthGalMap[44];
}
return lerp(depthGalMap[inchIndex], depthGalMap[inchIndex+1], (inches - inchIndex));
}
// Send an update by triggering a webhook on ifttt
void send(String val)
{
Serial.print("connecting to ");
Serial.println(host);
WiFiClient client;
const int httpPort = 80;
if (!client.connect(host, httpPort)) {
Serial.println("connection failed");
return;
}
String url = "/trigger/fuel_threshold/with/key/";
url += apiKey;
Serial.print("Requesting URL: ");
Serial.println(url);
client.print(String("POST ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
"Content-Length: 13\r\n\r\n" +
"value1=" + val + "\r\n");
}
void loop()
{
// Clears the trigger pin
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
// Sets the trigger pin on HIGH state for 10 micro seconds
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// Reads the echoPin, returns the sound wave travel time in microseconds
duration = pulseIn(echoPin, HIGH);
// Calculate the distance
distanceCm = duration * SOUND_SPEED/2;
// Convert to inches
distanceInch = distanceCm * CM_TO_INCH;
fillInches = 44.00 - distanceInch;
fillGallons = inchesToGallons(fillInches);
// Test values for when no proximity sensor is connected
// fillGallons = random(3500) / 100.00;
// Prints the distance in the Serial Monitor
if (TEST)
{
Serial.print("Fill Level (inches): ");
Serial.println(fillInches);
Serial.print("Fill Level (gallons): ");
Serial.println(fillGallons);
}
else if (fillGallons < threshold)
{
// Lower the threshold and trigger the webhook for alert
threshold -= 0.25;
send(String(fillGallons));
}
if (Serial.available())
{
while (Serial.available()==0){} // wait for user input
int removeCount = Serial.parseInt(); //Read user input and hold it in a variable
if (removeCount > 0)
{
String historyBackup[HISTORY_CAP];
for(int i = removeCount; i < HISTORY_CAP; i++)
{
pushHistory(historyBackup[i]);
}
}
}
// Save once daily; used in chart
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
char galBuf[6];
char dateBuf[9];
char historyBuf[13];
dtostrf(fillGallons, 6, 2, galBuf);
strftime(dateBuf, 9, "%m/%d/%y", &timeinfo); // DATE
// strftime(dateBuf, 9, "%H/%M/%S", &timeinfo); // TIME for testing; changes frequently
sprintf(historyBuf, "%s%s", dateBuf, galBuf);
if (historyCount == 0)
{
// Serial.print("First Recording: ");
// Serial.println(historyBuf);
pushHistory(historyBuf);
}
else
{
if (history[historyLatest].substring(0, 8) != dateBuf)
{
// Serial.print("Recording: ");
// Serial.println(historyBuf);
pushHistory(historyBuf);
DailyHistory();
}
}
}
else
{
Serial.println("Could not get date");
}
delay(intervalInSecs * 1000);
}