Mark Komus
Published © MIT

Weather Station Monitor with Adafruit IO

A companion project to my solar powered, Adafruit IO weather station. See the weather station's current readings on your desk.

BeginnerFull instructions provided8 hours3,002

Things used in this project

Hardware components

Adafruit Feather M0 Wifi
×1
Adafruit TPL5111 Low Power Timer Breakout
×1
Adafruit 2.13" Tri-Color eInk / ePaper Display with SRAM - Red Black White
×1
Adafruit Lithium Ion Polymer Battery - 3.7v 350mAh
×1

Software apps and online services

Adafruit IO
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Weather Display Case

Schematics

Weather Display Circuit Diagram

Code

Weather_Display.ino

C/C++
/******
The MIT License (MIT)

Copyright (c) 2020 Mark Komus

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 <RTCZero.h>
// Note on RTC

#include <SPI.h>
#include <WiFi101.h>
#include <Adafruit_GFX.h>    // Core graphics library
#include "Adafruit_EPD.h"

#include <Fonts/FreeSans7pt7b.h>
#include <Fonts/FreeSans8pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>

#include <ArduinoJson.h>
#include "secrets.h" 

#define EPD_CS     13
#define EPD_DC      12
#define SRAM_CS     11
#define EPD_RESET   10 // can set to -1 and share with microcontroller Reset!
#define EPD_BUSY    -1 // can set to -1 to not use a pin (will wait a fixed delay)
#define DONE_PIN  5 // For the TPL5111
#define VBAT_PIN A7

char ssid[] = SECRET_SSID;     // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)

int status = WL_IDLE_STATUS;

char server[] = "io.adafruit.com";    // name address for Google (using DNS)
char adafruit_user[] = "Gamblor21"; // user on Adafruit IO we are pulling requests from
char timezone[] = "America/Winnipeg"; // timezone to request from worldtimeapi.org

WiFiClient client;

Adafruit_IL0373 display(212, 104, EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY);

int nextAlarmMinute = 0;
volatile bool alarmWent = false;

void setup() {
  Serial.begin(115200);
  //while (!Serial) { ; }

  WiFi.setPins(8,7,4,2);

  // check for the presence of the shield:
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    // don't continue:
    while (true);
  }

  // attempt to connect to WiFi network:
  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    status = WiFi.begin(ssid, pass);
  }
  Serial.println("Connected to wifi");
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");

  // set the done pin for the TPL5111 to output
  pinMode(DONE_PIN, OUTPUT);
}

// Variables we will display
float avgtemp = 0.0;
float avgPressure = 0.0;
char p6Trend[20] = "\0";
float avgHumidity = 0.0;
float avgWindSpeed = 0.0;
int windDirection = 0.0;
char windDirectionString[5] = "\0";
float windGust = 0.0;
float totalRain = 0.0;  
float batVoltage = 0.0;
float measuredvbat = 0.0;
char currentTime[255] = "\0";

void loop() {
  // Read temperature
  sendHTTPRequest("temperature", 5, false, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 5000;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
    
      for (int i = 0; i < 5; i++) {
        avgtemp += doc[i]["value"].as<float>();
      }
      
      avgtemp /= 5.0;
    }
  }

  // Read Pressure
  sendHTTPRequest("pressure", 5, false, false);

  float currentPressure = 0.0;
  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 1350;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
    
      for (int i = 0; i < 5; i++) {
        avgPressure += doc[i]["value"].as<float>();
      }
      avgPressure /= 5.0;
      currentPressure = avgPressure;
    }
  }


  // Read 48 hours of pressure to get trends
  sendHTTPRequest("pressure", 0, false, true);  
  if (checkHTTPStatus()) {
    Serial.println("Pressure check");
    const size_t capacity = 49*JSON_ARRAY_SIZE(2) + JSON_ARRAY_SIZE(48) + JSON_OBJECT_SIZE(3) + 2*JSON_OBJECT_SIZE(5) + 2320;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
      
      JsonObject parameters = doc["parameters"];
      JsonArray data = doc["data"];

      // Look at several tends but right now only display the 6 hour trend
      float p48 = data[0][1].as<float>();
      float p24 = data[24][1].as<float>();
      float p12 = data[36][1].as<float>();
      float p6 = data[42][1].as<float>();
  
      float p48diff = currentPressure - p48;
      float p24diff = currentPressure - p24;
      float p12diff = currentPressure - p12;
      float p6diff = currentPressure - p6;
  
      if (abs(p6diff) > 0.35)
        sprintf(p6Trend, "Rapidly ");
  
      if(abs(p6diff) < 0.15) {
        sprintf(p6Trend, "Steady");
      }
      else if (p6diff > 0.15) {
        sprintf(p6Trend, "%sRising", p6Trend);
      }
      else if (p6diff < 0.15) {
        sprintf(p6Trend, "%sFalling", p6Trend);
      }
    }
  }

  // Read Humidty
  sendHTTPRequest("humidity", 5, false, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 1350;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
    
      for (int i = 0; i < 5; i++) {
        avgHumidity += doc[i]["value"].as<float>();
      }
      avgHumidity /= 5.0;
    }
  }

  // Reading wind speed
  sendHTTPRequest("wind-speed", 2, false, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 1350;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
    
      for (int i = 0; i < 2; i++) {
        avgWindSpeed += doc[i]["value"].as<float>();
      }
      avgWindSpeed /= 2.0;
    }
  }

  // Read wind gust
  sendHTTPRequest("wind-gust", 1, false, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 2000;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
  
      windGust = doc[0]["value"].as<float>();
    }
  }

  // Read wind direction
  sendHTTPRequest("wind-direction", 1, false, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 1350;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
  
      windDirection = doc[0]["value"].as<int>();
      windDirectionToString(windDirection, windDirectionString);
    }
  }
  
  // Reading rain in the last 60 minutes
  sendHTTPRequest("rain", 60, false, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 1350;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      readTrailer();
  
      for (int i = 0; i < 60; i++) {
        totalRain += doc[i]["value"].as<float>();
      }
    }
  }
  
  // Reading remote battery level
  sendHTTPRequest("battery-voltage", 1, true, false);

  if (checkHTTPStatus()) {
    const size_t capacity = 5*JSON_ARRAY_SIZE(3) + JSON_ARRAY_SIZE(5) + 10*JSON_OBJECT_SIZE(2) + 5*JSON_OBJECT_SIZE(11) + 1350;
    DynamicJsonDocument doc(capacity);
  
    // Parse JSON object
    DeserializationError error = deserializeJson(doc, client);
    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      client.stop();
    }
    else {
      batVoltage = doc[0]["value"].as<float>();
    }
  }

  client.stop();

  // Measure the local battery voltage
  measuredvbat = analogRead(VBAT_PIN);
  measuredvbat *= 2;    // we divided by 2, so multiply back
  measuredvbat *= 3.3;  // Multiply by 3.3V, our reference voltage
  measuredvbat /= 1024; // convert to voltage

  // Read the time from worldtimeapi.org
  getCurrentTimeFromWeb(currentTime);

  // Display all the values
  displayValues();

  // Signal the TPS5111 we are done and to power everything down
  digitalWrite(DONE_PIN, HIGH);
  delay(10);
  digitalWrite(DONE_PIN, LOW);
  delay(1000);

  Serial.println("We should never get here");
  while (true) { delay(1000); }
}

// Display all the values read to the eInk display
void displayValues() {
  char buf[255];
  int16_t x1, y1;
  uint16_t w, h;
  
  display.begin();
  display.clearBuffer();
  display.setTextWrap(false);

  display.setTextColor(EPD_BLACK);
  
  display.setTextSize(1);
  display.setFont(&FreeSansBold24pt7b);
  sprintf(buf, "%2.1fC", avgtemp);
  Serial.println(buf);
  display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(106-(w/2), 37);
  display.print(buf);

  display.setFont(&FreeSans9pt7b);
  sprintf(buf, "%2.1fkPa", avgPressure);
  Serial.println(buf);
  display.setCursor(2, 55);
  display.print(buf);

  display.setFont(&FreeSans7pt7b);
  sprintf(buf, "%s", p6Trend);
  Serial.println(buf);
  display.setCursor(5, 67);
  display.print(buf);

  display.setFont(&FreeSans9pt7b);
  sprintf(buf, "%2.1f %%RH", avgHumidity);
  Serial.println(buf);
  display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(208-w, 55);
  display.print(buf);

  display.setFont(&FreeSans8pt7b);
  sprintf(buf, "%2.1f / %2.1f km/h %s %d", avgWindSpeed, windGust, windDirectionString, windDirection);
  Serial.println(buf);
  display.setCursor(2, 85);
  display.print(buf);

  display.setFont(&FreeSans9pt7b);
  sprintf(buf, "%2.1fmm", totalRain);
  Serial.println(buf);
  display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(208-w, 70);
  //display.setCursor(2, 90);
  display.print(buf);

  display.setFont();
  display.setTextColor(EPD_RED);
  sprintf(buf, "Bat: %1.2f V", batVoltage);
  Serial.println(buf);
  display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(208-w, 94);
  display.print(buf);

  display.setFont();
  display.setCursor(2, 94);
  display.setTextColor(EPD_RED);
  sprintf(buf, "LBat: %1.2f V", measuredvbat);
  Serial.println(buf);
  display.print(buf);

  display.setFont(&FreeSans7pt7b);
  display.setTextColor(EPD_BLACK);
  Serial.println(currentTime);
  display.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(106-w, 94);
  display.setCursor(90, 100);
  display.print(currentTime);

  display.display();
}

// Read the data after the JSON request. We can then use keep-alives to reuse the connections
bool readTrailer() {
  char trailer[32] = {0};
  client.readBytesUntil('\r', trailer, sizeof(trailer));
  client.readBytesUntil('\r', trailer, sizeof(trailer));
  client.readBytesUntil('\r', trailer, sizeof(trailer));
  client.readBytesUntil('\r', trailer, sizeof(trailer));

  if (client.available() != 0)
    return false;

  return true;
}

// For a new connection make sure we got a 200 response and read the headers we are ignoring
bool checkHTTPStatus() {
    // Check HTTP status
    char status[32] = {0};
    client.readBytesUntil('\r', status, sizeof(status));
    if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
      Serial.print(F("Unexpected response: "));
      Serial.println(status);
      client.readBytesUntil('\r', status, sizeof(status));
      Serial.println(status);
      client.readBytesUntil('\r', status, sizeof(status));
      Serial.println(status);
      client.readBytesUntil('\r', status, sizeof(status));
      Serial.println(status);
      client.readBytesUntil('\r', status, sizeof(status));
      Serial.println(status);
      return false;
    }

    // Skip HTTP headers
    char endOfHeaders[] = "\r\n\r\n";
    if (!client.find(endOfHeaders)) {
      Serial.println(F("Invalid response"));
      return false;
    }

    char endOfLength[] = "\r\n";
    if (!client.find(endOfLength)) {
      Serial.println(F("Invalid response"));
      return false;
    }
}

// Send an HTTP request to Adafruit IO takes the feed name, the limit of how many records to return, if the connection
// should be kept open and do we want the raw data or a summary of 48 hours at 60 minute resolution
bool sendHTTPRequest(char* feed, byte limit, bool connClose, bool isChart) {
  // if not connected connect
  if (client.connected() == false) {
    if (client.connectSSL(server, 443)) {
      Serial.println(F("Client connected to Adafruit IO"));
    }
    else {
      Serial.println(F("Client failed to connect to Adafruit IO "));
      return false;
    }
  }
  
  char request[200];
  if (isChart == true) {
    // hard coded right now to 1 hour resolution for 48 hours or data
    sprintf(request, "GET /api/v2/%s/feeds/%s/data/chart?hours=48&resolution=60", adafruit_user, feed);
    sprintf(request, "%s HTTP/1.1", request);
   }
  else {  
    sprintf(request, "GET /api/v2/%s/feeds/%s/data?include=value", adafruit_user, feed);
    if (limit > 0)
      sprintf(request, "%s&limit=%d HTTP/1.1", request, limit);
    else
      sprintf(request, "%s HTTP/1.1", request);
  }
  
  client.println(request);
  client.println("Host: io.adafruit.com");
  
  if (connClose == true)
    client.println("Connection: close");
    
  client.println();

  return true;
}

// Gets the current time from worldtimeapi.org and save the time string to the timeString buffer
bool getCurrentTimeFromWeb(char* timeString) {
  if (client.connect("worldtimeapi.org", 80)) {
    Serial.println(F("Client connected to world time api"));
  }
  else {
    Serial.println(F("Client failed to connect to world time api"));
    return false;
  }

  char buf[255];
  sprintf(buf, "GET /api/timezone/%s HTTP/1.1", timezone);
  client.println(buf);
  client.println("Host: worldtimeapi.org");
  client.println("Accept: */*");
  client.println();

  char status[255] = {0};
  client.readBytesUntil('\r', status, sizeof(status));
  if (strcmp(status, "HTTP/1.1 200 OK") != 0) {
    Serial.print(F("Unexpected response: not 200"));
    return false;
  }

  char endOfHeaders[] = "\r\n\r\n";
  if (!client.find(endOfHeaders)) {
    Serial.println(F("Invalid response"));
    return false;
  }

  const size_t capacity = JSON_OBJECT_SIZE(15) + 360;
  DynamicJsonDocument doc(capacity);
  
  // Parse JSON object
  DeserializationError error = deserializeJson(doc, client);
  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.c_str());
    client.stop();
    return false;
  }
  
  const char* datetime = doc["datetime"]; // "2020-07-27T14:39:06.660033-05:00"
  
  memset(timeString, 0, strlen(timeString));
  strncpy(timeString, datetime+11, 5); 

  return true;
}

/* 
 *Translate the wind in degrees to a cardinal direction
 */
static char* directions[] = { "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW" };

bool windDirectionToString(int degrees, char *dirString) {
  int index = ((float)degrees + 11.25) / (float)22.5;
  //Serial.println(index);
  strcpy(dirString, directions[index%16]);
  
  return true;
}

Weather_Display.h

C Header File
#define SECRET_SSID "SSID"
#define SECRET_PASS "Password"

Weather Display

Credits

Mark Komus

Mark Komus

6 projects • 16 followers
Hobbyist tinkerer with all things related to technology as a break from my day job. @MarkKomus on twitter or @markkomus@mastodon.social

Comments