commit 455aefe8db63d32e1a09c05b3babb6e6173a1186 Author: Ano-sys Date: Sun Sep 27 00:41:41 2026 +0200 it was fun diff --git a/README.md b/README.md new file mode 100644 index 0000000..767e87a --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Awair Data Grabber + +## Disclaimer + +This project retrieves sensor data from an Awair Element through its enabled Local API and displays it on an 128x64 OLED. The IdeaSpark ESP8266 board uses different OLED pins than a standard NodeMCU ESP8266. + +## Requirements + +- ESP8266 Arduino core +- ArduinoJson +- U8g2 + +## Usage + +- enable the Local API for your Awair Element in the Awair Home app +- set `OLED_SCL` and `OLED_SDA` according to your board specification, the current defaults are GPIO 14 and GPIO 12 +- set `SSID` and `PASSWD` for a network that can reach the Awair Element +- set the full `SERVER_URL`, for example `http://awair-elem-123456.local/air-data/latest` +- flash the firmware, the sensor data will be displayed after a successful request diff --git a/awair_data_grabber/awair_data_grabber.ino b/awair_data_grabber/awair_data_grabber.ino new file mode 100644 index 0000000..5a35604 --- /dev/null +++ b/awair_data_grabber/awair_data_grabber.ino @@ -0,0 +1,212 @@ +#include +#include +#include +#include + +#ifdef U8X8_HAVE_HW_SPI +#include +#endif +#ifdef U8X8_HAVE_HW_I2C +#include +#endif + +#define OLED_SCL 14 +#define OLED_SDA 12 + +#define UPDATE_INTERVAL_MS 1000 + +#define SERVER_URL "http://awair-server/air-data/latest" + +#define SSID "-" +#define PASSWD "-" + +WiFiClient client; +HTTPClient http; + +struct AwairData{ + private: + int score = 0; + float temp = .0f; + float humid = .0f; + int co2 = 0; + int voc = 0; + int pm25 = 0; + + bool updated = false; + + template + void update_var(T* var, T value){ + if(*var == value) return; + *var = value; + updated = true; + } + + public: + void set_score(int value){ update_var(&score, value); } + void set_temp(float value){ update_var(&temp, value); } + void set_humid(float value){ update_var(&humid, value); } + void set_co2(int value){ update_var(&co2, value); } + void set_voc(int value){ update_var(&voc, value); } + void set_pm25(int value){ update_var(&pm25, value); } + + int get_score(void) const { return score; } + float get_temp(void) const { return temp; } + float get_humid(void) const { return humid; } + int get_co2(void) const { return co2; } + int get_voc(void) const { return voc; } + int get_pm25(void) const { return pm25; } + + void update(int score, float temp, float humid, int co2, int voc, int pm25){ + set_score(score); + set_temp(temp); + set_humid(humid); + set_co2(co2); + set_voc(voc); + set_pm25(pm25); + } + + bool has_updated(void) const { return updated; } + void reset_updated(void){ updated = false; } +}; + +class Panel{ + private: + U8G2_SSD1306_128X64_NONAME_F_SW_I2C panel; + + public: + Panel() : panel(U8G2_R0, OLED_SCL, OLED_SDA, U8X8_PIN_NONE){} + + void begin(void){ + panel.begin(); + } + + void show_failed(void){ + panel.clearBuffer(); + panel.setFont(u8g2_font_ncenB08_tr); + panel.drawStr(0, 10, "Something failed"); + panel.sendBuffer(); + } + + void update(const AwairData& data){ + char buffer[16]; + + panel.clearBuffer(); + + panel.setFont(u8g2_font_ncenB08_tr); + panel.drawStr(0, 9, "Score:"); + + snprintf(buffer, sizeof(buffer), "%d", data.get_score()); + panel.drawStr(42, 9, buffer); + + panel.drawHLine(0, 12, 128); + + panel.setFont(u8g2_font_6x10_tr); + + panel.drawStr(0, 24, "Temp"); + snprintf(buffer, sizeof(buffer), "%.1f C", data.get_temp()); + panel.drawStr(0, 35, buffer); + + panel.drawStr(68, 24, "Humid"); + snprintf(buffer, sizeof(buffer), "%.1f %%", data.get_humid()); + panel.drawStr(68, 35, buffer); + + panel.drawStr(0, 47, "CO2"); + panel.drawStr(47, 47, "VOC"); + panel.drawStr(87, 47, "PM2.5"); + + snprintf(buffer, sizeof(buffer), "%d", data.get_co2()); + panel.drawStr(0, 59, buffer); + + snprintf(buffer, sizeof(buffer), "%d", data.get_voc()); + panel.drawStr(47, 59, buffer); + + snprintf(buffer, sizeof(buffer), "%d", data.get_pm25()); + panel.drawStr(87, 59, buffer); + + panel.sendBuffer(); + } +}; + +bool parseAwairJson(const String& payload, AwairData& data) { + JsonDocument doc; + + auto error = deserializeJson(doc, payload); + if(error){ + Serial.print("JSON error: "); + Serial.println(error.c_str()); + return false; + } + + int score = doc["score"] | 0; + float temp = doc["temp"] | .0f; + float humid = doc["humid"] | .0f; + int co2 = doc["co2"] | 0; + int voc = doc["voc"] | 0; + int pm25 = doc["pm25"] | 0; + + data.update(score, temp, humid, co2, voc, pm25); + + return true; +} + +int request_data(AwairData* data){ + if(!http.begin(client, SERVER_URL)){ + Serial.println("Failed to request URL!"); + return 1; + } + + int http_code = http.GET(); + int result = 1; + if(http_code == HTTP_CODE_OK){ + if(parseAwairJson(http.getString(), *data)) result = 0; + } + else{ + Serial.print("Failed GET request: "); + Serial.println(http_code, 10); + } + + http.end(); + + return result; +} + +Panel panel; +AwairData data; + +void setup() { + Serial.begin(115200); + + panel.begin(); + + http.setTimeout(3000); + + WiFi.begin(SSID, PASSWD); + while(WiFi.status() != WL_CONNECTED) delay(100); + + Serial.println("Connected!"); +} + +void loop() { + /* + static auto start = millis() - UPDATE_INTERVAL_MS; + auto current = millis(); + + if(current - start < UPDATE_INTERVAL_MS) return; + start = current; + */ + + if(WiFi.status() != WL_CONNECTED){ delay(100); return; } + + Serial.println("Requesting data..."); + if(request_data(&data) != 0){ panel.show_failed(); } + + if(data.has_updated()){ + Serial.println("Got new data -> Updating..."); + panel.update(data); + data.reset_updated(); + } + else Serial.println("Got data but nothing new!"); + + Serial.println("---------- ---------- ----------\n"); + delay(UPDATE_INTERVAL_MS); // simple delay is enough <- no state machine +}