Skip to content

Instantly share code, notes, and snippets.

@dfsnow
Last active August 1, 2023 19:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfsnow/54067b37341f4f4589e53d15c8a37d17 to your computer and use it in GitHub Desktop.
Save dfsnow/54067b37341f4f4589e53d15c8a37d17 to your computer and use it in GitHub Desktop.
Arduino sketch to export Prometheus metrics from the AG DIY Pro V3.7, based on Jeff Geerling's code
#include <AirGradient.h>
#include <WiFiManager.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WiFiClient.h>
#include <EEPROM.h>
#include <SensirionI2CSgp41.h>
#include <NOxGasIndexAlgorithm.h>
#include <VOCGasIndexAlgorithm.h>
#include <U8g2lib.h>
AirGradient ag = AirGradient();
SensirionI2CSgp41 sgp41;
VOCGasIndexAlgorithm voc_algorithm;
NOxGasIndexAlgorithm nox_algorithm;
// time in seconds needed for NOx conditioning
uint16_t conditioning_s = 10;
// Display bottom right
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// CONFIGURATION START
// set the device name when printing metrics
const char* deviceId = "CHANGE ME";
// hardware options for AirGradient DIY sensor
const bool hasTVOC = true;
const bool hasPM = true;
const bool hasCO2 = true;
const bool hasSHT = true;
// set to true to switch from Celcius to Fahrenheit
const bool inF = true;
// PM2.5 in US AQI (default ug/m3)
const bool inUSAQI = true;
// Display Position
const bool displayTop = false;
// connection settings
const char* ssid = "CHANGE ME";
const char* password = "CHANGE ME";
const int port = 9926;
// update frequency of endpoint
const int updateFrequency = 5000;
// CONFIGURATION END
unsigned long currentMillis = 0;
const int oledInterval = 5000;
unsigned long previousOled = 0;
const int sendToServerInterval = 5000;
unsigned long previoussendToServer = 0;
const int tvocInterval = 1000;
unsigned long previousTVOC = 0;
int TVOC = 0;
int NOX = 0;
const int co2Interval = 5000;
unsigned long previousCo2 = 0;
int Co2 = 0;
const int pm25Interval = 5000;
unsigned long previousPm25 = 0;
int pm25 = 0;
const int tempHumInterval = 2500;
unsigned long previousTempHum = 0;
float temp = 0;
int hum = 0;
ESP8266WebServer server(port);
void setup() {
Serial.begin(115200);
Serial.println("Hello");
u8g2.setBusClock(100000);
u8g2.begin();
//u8g2.setDisplayRotation(U8G2_R0);
EEPROM.begin(512);
delay(500);
updateOLED2("Warming up the", "sensors...", "");
if (hasTVOC) sgp41.begin(Wire);
if (hasPM) ag.PMS_Init();
if (hasCO2) ag.CO2_Init();
if (hasSHT) ag.TMP_RH_Init(0x44);
// set WiFi mode to client (without this it may try to act as an AP)
WiFi.mode(WIFI_STA);
// configure hostname
if ((deviceId != NULL) && (deviceId[0] == '\0')) {
Serial.printf("No Device ID is Defined, Defaulting to board defaults");
}
else {
wifi_station_set_hostname(deviceId);
WiFi.setHostname(deviceId);
}
// setup and wait for WiFi
WiFi.begin(ssid, password);
Serial.println("");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
updateOLED2("Trying to", "connect...", "");
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Serial.print("MAC address: ");
Serial.println(WiFi.macAddress());
Serial.print("Hostname: ");
Serial.println(WiFi.hostname());
server.on("/", HandleRoot);
server.on("/metrics", HandleRoot);
server.onNotFound(HandleNotFound);
server.begin();
Serial.println("HTTP server started at ip " + WiFi.localIP().toString() + ":" + String(port));
}
void loop() {
currentMillis = millis();
updateTVOC();
updateOLED();
updateCo2();
updatePm25();
updateTempHum();
server.handleClient();
}
void updateTVOC()
{
uint16_t error;
char errorMessage[256];
uint16_t defaultRh = 0x8000;
uint16_t defaultT = 0x6666;
uint16_t srawVoc = 0;
uint16_t srawNox = 0;
uint16_t defaultCompenstaionRh = 0x8000; // in ticks as defined by SGP41
uint16_t defaultCompenstaionT = 0x6666; // in ticks as defined by SGP41
uint16_t compensationRh = 0; // in ticks as defined by SGP41
uint16_t compensationT = 0; // in ticks as defined by SGP41
delay(1000);
compensationT = static_cast<uint16_t>((temp + 45) * 65535 / 175);
compensationRh = static_cast<uint16_t>(hum * 65535 / 100);
if (conditioning_s > 0) {
error = sgp41.executeConditioning(compensationRh, compensationT, srawVoc);
conditioning_s--;
} else {
error = sgp41.measureRawSignals(compensationRh, compensationT, srawVoc,
srawNox);
}
if (currentMillis - previousTVOC >= tvocInterval) {
previousTVOC += tvocInterval;
TVOC = voc_algorithm.process(srawVoc);
NOX = nox_algorithm.process(srawNox);
Serial.println(String(TVOC));
}
}
void updateCo2()
{
if (currentMillis - previousCo2 >= co2Interval) {
previousCo2 += co2Interval;
Co2 = ag.getCO2_Raw();
Serial.println(String(Co2));
}
}
void updatePm25()
{
if (currentMillis - previousPm25 >= pm25Interval) {
previousPm25 += pm25Interval;
pm25 = ag.getPM2_Raw();
Serial.println(String(pm25));
}
}
void updateTempHum()
{
if (currentMillis - previousTempHum >= tempHumInterval) {
previousTempHum += tempHumInterval;
TMP_RH result = ag.periodicFetchData();
temp = result.t;
hum = result.rh;
Serial.println(String(temp));
}
}
void updateOLED() {
if (currentMillis - previousOled >= oledInterval) {
previousOled += oledInterval;
String ln3;
String ln1;
if (inUSAQI) {
ln1 = "AQI:" + String(PM_TO_AQI_US(pm25)) + " CO2:" + String(Co2);
} else {
ln1 = "PM:" + String(pm25) + " CO2:" + String(Co2);
}
String ln2 = "TVOC:" + String(TVOC) + " NOX:" + String(NOX);
if (inF) {
ln3 = "F:" + String((temp* 9 / 5) + 32) + " H:" + String(hum)+"%";
} else {
ln3 = "C:" + String(temp) + " H:" + String(hum)+"%";
}
updateOLED2(ln1, ln2, ln3);
}
}
void updateOLED2(String ln1, String ln2, String ln3) {
char buf[9];
u8g2.firstPage();
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_t0_16_tf);
u8g2.drawStr(1, 10, String(ln1).c_str());
u8g2.drawStr(1, 30, String(ln2).c_str());
u8g2.drawStr(1, 50, String(ln3).c_str());
} while ( u8g2.nextPage() );
}
String fetchMetrics() {
String message = "";
String idString = "{id=\"" + String(deviceId) + "\",mac=\"" + WiFi.macAddress().c_str() + "\"}";
if (hasPM) {
message += "# HELP pm02 Particulate Matter PM2.5 value\n";
message += "# TYPE pm02 gauge\n";
message += "pm02";
message += idString;
if (inUSAQI) {
message += String(PM_TO_AQI_US(pm25));
} else {
message += String(pm25);
}
message += "\n";
}
if (hasTVOC) {
message += "# HELP tvoc TVOC value, index\n";
message += "# TYPE tvoc gauge\n";
message += "tvoc";
message += idString;
message += String(TVOC);
message += "\n";
message += "# HELP nox NOX value, index\n";
message += "# TYPE nox gauge\n";
message += "nox";
message += idString;
message += String(NOX);
message += "\n";
}
if (hasCO2) {
message += "# HELP rco2 CO2 value, in ppm\n";
message += "# TYPE rco2 gauge\n";
message += "rco2";
message += idString;
message += String(Co2);
message += "\n";
}
if (hasSHT) {
message += "# HELP atmp Temperature, in degrees\n";
message += "# TYPE atmp gauge\n";
message += "atmp";
message += idString;
if (inF) {
message += String((temp* 9 / 5) + 32);
} else {
message += String(temp);
}
message += "\n";
message += "# HELP rhum Relative humidity, in percent\n";
message += "# TYPE rhum gauge\n";
message += "rhum";
message += idString;
message += String(hum);
message += "\n";
}
return message;
}
void HandleRoot() {
if (currentMillis - previoussendToServer >= sendToServerInterval) {
previoussendToServer += sendToServerInterval;
server.send(200, "text/plain", fetchMetrics() );
}
}
void HandleNotFound() {
String message = "File Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET) ? "GET" : "POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint i = 0; i < server.args(); i++) {
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/html", message);
}
// Calculate PM2.5 US AQI
int PM_TO_AQI_US(int pm02) {
if (pm02 <= 12.0) return ((50 - 0) / (12.0 - .0) * (pm02 - .0) + 0);
else if (pm02 <= 35.4) return ((100 - 50) / (35.4 - 12.0) * (pm02 - 12.0) + 50);
else if (pm02 <= 55.4) return ((150 - 100) / (55.4 - 35.4) * (pm02 - 35.4) + 100);
else if (pm02 <= 150.4) return ((200 - 150) / (150.4 - 55.4) * (pm02 - 55.4) + 150);
else if (pm02 <= 250.4) return ((300 - 200) / (250.4 - 150.4) * (pm02 - 150.4) + 200);
else if (pm02 <= 350.4) return ((400 - 300) / (350.4 - 250.4) * (pm02 - 250.4) + 300);
else if (pm02 <= 500.4) return ((500 - 400) / (500.4 - 350.4) * (pm02 - 350.4) + 400);
else return 500;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment