WEATHERMAN: Web Weather Forecaster

How many times have you asked yourself “What’s the weather going to be like today and over the next couple of days?” Then you have to turn on your computer, pull up a web page with a weather forecast, and wade through all the ads.  Yep, I know. Pretty tiring and time-consuming.

So now, the wait is over!

Introducing “Weatherman: Web Weather Forecaster“, the web-based three-day weather forecaster that provides all you ever wanted to know, right at your fingertips… literally!

Weatherman

Weatherman: Web Weather Forecaster showing today’s weather conditions. A low of 15 to a high of 19.6 degrees C, 53% humidity and a pressure of 1000mb, with overcast clouds and winds from the SSW at 1.9kph.

Weatherman: Web Weather Forecaster showing

Weatherman: Web Weather Forecaster showing tomorrow’s weather

Weatherman: Web Weather Forecaster showing

Weatherman: Web Weather Forecaster showing Monday’s weather

In this version of the design I used an Arduino UNO and an Ethernet shield connected – via a power-line Ethernet extender – to my Internet router LAN. A 2-wire I2C interface was used to connect a 4×20 character LCD and an RTC module, and a single pin used for a small capacitive touch-pad, the latter being used to turn on the display and cycle through the weather forecast for today and two additional days. The whole stack (UNO, Ethernet shield and RTC/PSU board) was mounted between two pieces of plexiglass. The software was written so that when a finger presses the front panel touch pad , the back light of the display is turned on and today’s forecast data is displayed for 20 seconds. Subsequent presses cycle through the three days worth of weather data.

Periodically, the software invokes an api by www.openweathermap.org that returns a JSON-formatted string of data containing a 7-day forecast. As the complete returned data string is too large for the limited amount of memory of the Arduino (2kB), the data has to be ‘culled’ in a number of ways. First, the first 150 characters are ignored (as these contain data about the location); secondly, all quotation marks (“), curly braces ({}) and square brackets ([]) are ignored; and finally, only the subsequent 650 characters are collected. This “pruned’ data string provides sufficient data for the weather for the first three days’ forecast.

The received data string is then parsed for high and low daily temperatures (ºC), humidity (%) and pressure (mb), wind speed (kph) and direction (cardinal) and a text-based description of conditions (such as “moderate rain”, “heavy intensity rain” and “overcast clouds”, as seen in the images above).

While there are other data available, you have to strike a compromise between the small amount of available SRAM and the limits of the display size. Collecting between 620-650 characters of data from the JSON stream, the UNO appears to operate reliably. Increasing the number of characters collected led to unusual and unstable operation.

Anyway, the “Weatherman: Web Weather Forecaster” works well, and is eminently hackable! I wish you all good weather!

Here’s the Arduino code…

[codesyntax lang=”php” title=”Weatherman: Web Weather Forecaster: Build 1.” blockstate=”collapsed”]

//**************************************************************************************************
//                                 WEB WEATHER FORECAST MONITOR
//                                   Adrian Jones, June 2015
//
//**************************************************************************************************

// Build 1
//   r1 150320 - initial build with I2C 4x20 display and RTC
//   r2 150617 - new string parsing, button to cycle through days of forecast
//   r3 150618 - temperature highs and lows

//********************************************************************************************
#define build 1
#define revision 3
//********************************************************************************************

#include <avr/pgmspace.h>            // for memory storage in program space

#include <Wire.h> 
#include <LCD.h>                     // Standard lcd library
#include <LiquidCrystal_I2C.h>       // library for I@C interface

#define I2C_ADDR  0x27               // address found from I2C scanner
#define RS_pin    0                  // pin configuration for LCM1602 interface module
#define RW_pin    1
#define EN_pin    2
#define BACK_pin  3
#define D4_pin    4
#define D5_pin    5
#define D6_pin    6
#define D7_pin    7
#define backON    0xFF
#define backOFF   0x00

LiquidCrystal_I2C lcd(I2C_ADDR, EN_pin, RW_pin, RS_pin, D4_pin, D5_pin, D6_pin, D7_pin, BACK_pin, POSITIVE);
//Pins for the LCD are SCL A5, SDA A4

#include <SPI.h>
#include <Ethernet.h>

byte mac[] = {*** YOUR MAC ADDRESS ***};
IPAddress ip(*** YOUR IP ADDRESS ***);            // LAN IP address (http://192.168.1.178:8081)
IPAddress gateway(*** YOUR GATEWAY ADDRESS ***);  // internet access via router
IPAddress subnet(255,255,255,0);                  // subnet mask

EthernetClient client;

char server[]   = "api.openweathermap.org";                  // name for server
String sendReq  = "/data/2.5/forecast/daily?id=YOUR_LOCATION&units=metric&APPID=YOUR_AAIP";     // send request

String inString = "";
char inChar;
#define longDelay     600                          // long delay (s)
#define shortDelay     30                          // short delay (s)
int loopTime;                                      // loop time (s)
#define timeout      3000                          // timeout for receiving server data

const char custom[][8] PROGMEM = {
     { 0x01, 0x07, 0x0F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F },      // char 1: bottom right triangle
     { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F },      // char 2: bottom block
     { 0x10, 0x1C, 0x1E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F },      // char 3: bottom left triangle
     { 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x0F, 0x07, 0x01 },      // char 4: top right triangle
     { 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1E, 0x1C, 0x10 },      // char 5: top left triangle
     { 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00 },      // char 6: upper block
     { 0x01, 0x07, 0x0F, 0x1F, 0x00, 0x00, 0x00, 0x00 },      // char 7: top right triangle
     { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x0F, 0x07, 0x01 }       // char 8: lower right triangle
};

const char bn3[][30] PROGMEM = {                            // 3-line numbers
//         0               1               2               3               4              5               6                7               8               9
    { 0x01,0x06,0x03, 0x07,0xFF,0xFE, 0x07,0x06,0x03, 0x07,0x06,0x03, 0xFF,0xFE,0xFF, 0xFF,0x06,0x06, 0x01,0x06,0xFE, 0x06,0x06,0xFF, 0x01,0x06,0x03, 0x01,0x06,0x03 },
    { 0xFF,0xFE,0xFF, 0xFE,0xFF,0xFE, 0x01,0x06,0x06, 0xFE,0x06,0xFF, 0x06,0x06,0xFF, 0x06,0x06,0x03, 0xFF,0x06,0x03, 0xFE,0x01,0x05, 0xFF,0x06,0xFF, 0x06,0x06,0xFF },
    { 0x04,0x02,0x05, 0xFE,0xFF,0xFE, 0xFF,0x02,0x02, 0x08,0x02,0x05, 0xFE,0xFE,0xFF, 0x04,0x02,0x05, 0x04,0x02,0x05, 0xFE,0xFF,0xFE, 0x04,0x02,0x05, 0x08,0x02,0x05 }
};


byte row,nb=0,bc=0,bb[8];

// cardinal directions
const char card0[]  PROGMEM = "N";
const char card1[]  PROGMEM = "NNE";
const char card2[]  PROGMEM = "NE";
const char card3[]  PROGMEM = "ENE";
const char card4[]  PROGMEM = "E";
const char card5[]  PROGMEM = "ESE";
const char card6[]  PROGMEM = "SE";
const char card7[]  PROGMEM = "SSE";
const char card8[]  PROGMEM = "S";
const char card9[]  PROGMEM = "SSW";
const char cardA[]  PROGMEM = "SW";
const char cardB[]  PROGMEM = "WSW";
const char cardC[]  PROGMEM = "W";
const char cardD[]  PROGMEM = "WNW";
const char cardE[]  PROGMEM = "NW";
const char cardF[]  PROGMEM = "NNW";
const char * const cards[] PROGMEM = { card0,card1,card2,card3,card4,card5,card6,card7,card8,card9,cardA,cardB,cardC,cardD,cardE,cardF };

int s;
float da[3],ni[3],pr[3],hu[3],ws[3],wd[3];
String de[3];
byte x;
boolean newdata=false;
#define ignore 160                                    // characters to ignore

// mode switch 
#define sw 3                                          // mode switch pin
#define today     0                                   
#define tomorrow  1
#define nextday   2
#define nummodes  3                                  // number of modes
boolean swdown = false;                              // switch depressed?
byte mode=today, omode=nummodes, mdelay;
#define swdelay 20                                    // delay (secs) before returning to today

const char d0[]  PROGMEM = "Sun";
const char d1[]  PROGMEM = "Mon";
const char d2[]  PROGMEM = "Tue";
const char d3[]  PROGMEM = "Wedn";
const char d4[]  PROGMEM = "Thu";
const char d5[]  PROGMEM = "Fri";
const char d6[]  PROGMEM = "Sat";
const char * const daytxt[] PROGMEM = { d0,d1,d2,d3,d4,d5,d6 };
char cb[4];                                        // character buffer 

// RTC
#include "RTClib.h"
RTC_DS1307 RTC;
byte hr, mn, se, osec, dy, mo, yr, dw;

// eeprom storage
#include <EEPROM.h>                                // for data storage
int dst;                                           // dst setting                      
int *set[] = { &dst };                             // values to set/recover from EEPROM

#define serEn true                                 // serial port
#define sp Serial.print
#define spln Serial.println

#define lcdEn true 
#define lp lcd.print
#define ls lcd.setCursor
#define lw lcd.write
#define lc lcd.clear

//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup() {
  Ethernet.begin(mac, ip, gateway, gateway, subnet);                  // set up connection
  //Ethernet.begin(mac);
  delay(2000);
  
  Wire.begin();
  RTC.begin();
  if (! RTC.isrunning()) {
    spln(F("RTC is NOT running!"));
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
  // RTC.adjust(DateTime(__DATE__, __TIME__)); 
  // RTC.adjust(DateTime(2015, 11, 1, 1, 59, 50 ));     // test 

  
  if(serEn) {
    Serial.begin(57600);                                             // Open serial port
    spln(F("********************************************"));
    spln(F("WEB WEATHER FORECAST MONITOR by Adrian Jones"));
    sp(F("Build ")); sp(build); sp(F(".")); spln(revision);
    sp(F("Free RAM: "));  sp(freeRam()); spln(F("B"));
    spln(F("********************************************"));
  }
  
  if(lcdEn) {
    lcd.begin(20, 4);
    for (nb=0; nb<8; nb++ ) {                     // create 8 custom characters
      for (bc=0; bc<8; bc++) bb[bc]= pgm_read_byte( &custom[nb][bc] );
      lcd.createChar ( nb+1, bb );
    }
    lc();
    ls(0, 0); lp(F("WEB WEATHER STATION")); 
    ls(5, 1); lp(F("V")); lp(build); lp(F(".")); lp(revision); lp(F(" "));  lp(freeRam()); lp(F("B"));
    ls(2, 2); lp(F("... waiting ..."));
  }
  
  restoreSettings();                      // restore EEPROM settings 
  delay(1000);
  loopTime = 0;                          // initially
  mdelay=swdelay;
  pinMode(sw, INPUT);                    // switch input
}

//*****************************************************************************************//
//                                      MAIN LOOP
//*****************************************************************************************//

void loop() { 
  getTime();                                          // get current time  
  doDSTAdjust();                                      // and do DST adjustr if necessary
  doSwitch();                                         // check switches 
  lcd.setBacklight(mdelay?backON:backOFF);

  if(se != osec) {  
    // main loop to get weather forecast data from server 
    if(--loopTime < 0) {                              // if ready      
      loopTime = doConnect()? longDelay: shortDelay;  // connect to server. If OK, long delay
      inString = "";
      s=0;                                            // ignore character count
      long timenow = millis();                        // set timeout counter
      while( (timenow+timeout) > millis() ) {
        boolean done=false;
        while (client.available()) {                  // while there are some... 
          inChar = client.read();                     // read incoming bytes if available
          if(s++ >ignore && inChar!=char(0x22) && inChar!=char(0x5B) && inChar!=char(0x5D) && inChar!=char(0x7B) && inChar!=char(0x7D) && !done) inString += inChar;    // ignore first chars and remove quotation marks["]
          if( !done && inString.length()>610 ) { inString +="\n"; done=true; }  // if string length has first three days, you're done
        }
      }

      if(inString.length()>300) {                     // Parse new input string
        sp("inString = "); spln(inString);
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("day:",s)+4;       da[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("night:", s)+6;    ni[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("pressure:", s)+9; pr[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("humidity:", s)+9; hu[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("speed:", s)+6;    ws[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("deg:", s)+4;      wd[x]=inString.substring(s,(inString.indexOf(',',s))).toFloat(); }
        s=0; for(x=0;x<3;x++) { s=inString.indexOf("cription:", s)+9; de[x]=inString.substring(s,(inString.indexOf(',',s)));  }
        newdata=true;   
        mode=today;
      }
      if (!client.connected()) { spln(F("-> Disconnected")); client.stop(); } // if the server's disconnected, stop the client
    }
    // end of main loop to get weather forecast data from server 
    
    doSerial();
    doLCD();
    doLoopCount();
    newdata=false;
    omode=mode;
    osec=se;
    if(mdelay>0) mdelay--;
  }   
  delay(50);  
}


// ********************************************************************************** //
//                                  CONNECTION SUBROUTINES
// ********************************************************************************** //

boolean doConnect() {
  if(serEn) spln(F("Connecting to server"));
  if (client.connect(server, 80)) {
    if(serEn) spln(F("-> Connected\n\r"));             // if you get a connection, report back via serial:
    client.print(F("GET "));
    client.println(sendReq);
    client.println(F("Host: api.openweathermap.org"));
    client.println(F("Connection: close")); 
    client.println();
    return true;  
  } else {
    if(serEn) spln(F("-> Connection failed"));        // if you didn't get a connection to the server
    return false;
  }
}


// ********************************************************************************** //
//                                     SERIAL OUTPUT
// ********************************************************************************** //
void doSerial() {
  if(!serEn) return;
  if(mode == omode && !newdata) return;          // nothing new to report
  
  if(mode==0) spln(F("\nToday")); else spln( getDayOfWeek((dw+mode)%7) );
  sp(F("Temperature: \tHi ")); sp(da[mode],1); sp(F("C to Lo ")); spln(ni[mode],1); sp(F("C"));    
  sp(F("Humidity:\t"));         sp(hu[mode],0); spln(F("%"));
  sp(F("Pressure:\t"));         sp(pr[mode],0); spln(F(" hpa"));
  sp(F("Wind speed:\t"));       sp(ws[mode],1); spln(F(" kph"));
  sp(F("Wind Direction:\t"));   sp(wd[mode],0); sp(F(" deg [")); sp(getCardinal(wd[mode])); spln(F("]"));
  sp(F("Conditions:\t"));       spln(de[mode]);
}

// ********************************************************************************** //
//                                     LCD OUTPUT
// ********************************************************************************** //
void doLCD() {
  if(!lcdEn) return;
  if(mode == omode && !newdata) return;

  lc();                                                                            // clear all first
  ls(0, 0);  if(mode==0) lp(F("Today")); else lp( getDayOfWeek((dw+mode)%7) );     // day of week
  lp(F(" (")); lp(int(ni[mode])); lp(F(") ")); 
  if(da[mode]<0.0) { if(abs(da[mode])>9) ls(11, 1); else ls(13, 1); lw(0x06); }    // negative reading?
  if(abs(da[mode]) > 9) printNum3(abs(da[mode])/10, 12, 0);                        // double digit?
  printNum3(abs(int(da[mode]))%10, 15, 0);                                         // temp units
  ls(19, 2); lp(int(da[mode]*10)%10);                                              // single decimal place
  ls(18, 0); lw(0xDF);   lp(F("C"));                                               // deg. C
  
  ls(0, 1); lp(hu[mode],0); lp(F("% "));   lp(pr[mode],0); lp(F("mb"));            // humidity & pressure
  ls(0, 2); lp(ws[mode],1); lp(F("kph ")); lp(getCardinal(wd[mode]));              // wind speed and direction
  ls(0, 3); lp(de[mode].substring(0,20));                                          // write out description (cut to 17 chars)
}


void doLoopCount() {
  if(!lcdEn) return;
  ls(18, 2); lp( (se%2)?F("."):F(" ")); 
/*
  ls(18, 3); 
  if(loopTime<60) {
    if(loopTime<10) lp(F(" ")); lp(loopTime);                                     // count down last 60 seconds
  } else {
    if((loopTime/60)+1<10) {                                                      // count down mins
       lp((loopTime/60)+1); lp( (se%2)?".":" ");                                  // count down mins <9; pulse 
    } else {
       lp((loopTime/60)+1);
    }
  }
  */
}


String getDayOfWeek(byte dow ) { strcpy_P(cb, (char*) pgm_read_word(&daytxt[dow])); return cb; }

void doSwitch() { 
  if(!digitalRead(sw) && swdown) {                 // button released and previously pressed
    if(mdelay != 0) mode=(mode+1)%nummodes;        // cycle through modes
    mdelay=swdelay;                                // start mode delay
    swdown=false;                                  // reset switch down
  } 
  if(digitalRead(sw)) swdown=true;                 // button pressed
  if(mdelay==1) mode=today;                        // reset to today
}


// ********************************************************************************** //
//                                      OPERATION ROUTINES
// ********************************************************************************** //
// FREERAM: Returns the number of bytes currently free in RAM  
int freeRam(void) {
  extern int  __bss_end, *__brkval; 
  int free_memory; 
  if((int)__brkval == 0) {
    free_memory = ((int)&free_memory) - ((int)&__bss_end); 
  } 
  else {
    free_memory = ((int)&free_memory) - ((int)__brkval); 
  }
  return free_memory; 
}

// ********************************************************************************** //
//                               GENERAL SUBROUTINES
// ********************************************************************************** //
// getCardinal: converts the headsing (in degrees) to a text-based cardinal heading ("ENE", for example)
String getCardinal( float h ) { 
  h += 11.25; if (h > 360.0) h = h-360.0;
  strcpy_P(cb, (char*) pgm_read_word(&(cards[(int) (h / 22.5)]) ) );
  return cb;  
}

// ********************************************************************************** //
//                             LCD PRINT SUBROUTINES
// ********************************************************************************** //
void printNum3(byte digit, byte leftAdjust, byte topAdjust) {
   for(row=0; row<3; row++) {
     ls(leftAdjust,row+topAdjust);               
     for(byte num=digit*3; num <digit*3+3; num++) {
       lw(pgm_read_byte( &bn3[row][num]) );
     }
   }
}

/* ********************************************************************************** //
//                                       TIME ROUTINES
// ********************************************************************************** //
   In most of Canada Daylight Saving Time begins at 2:00 a.m. local time on the second Sunday in March. 
   On the first Sunday in November areas on DST return to Standard Time at 2:00 a.m. local time. 
*/

// IsDST returns true if DST, false otherwise
boolean IsDST() {
  if (mo < 3 || mo > 11) { return false; }                // January, February, and December are out.
  if (mo > 3 && mo < 11) { return true;  }                // April to October are in
  int previousSunday = dy - dw;                               
  if (mo == 3) { return previousSunday >= 8; }            // In March, we are DST if our previous Sunday was on or after the 8th.
  return previousSunday <= 0;                             // In November we must be before the first Sunday to be DST. That means the previous Sunday must be before the 1st.
}

void getTime() {
  DateTime now = RTC.now();
  hr = now.hour(); if(hr==0) hr=24;
  mn = now.minute();
  se = now.second();
  yr = now.year();
  mo = now.month();
  dy = now.day();
  dw = now.dayOfWeek();
}

// doDSTAdjust increments (or decrements) by one hour when entering (or leaving) DST
void doDSTAdjust() {
  if(dst == IsDST()) return;                    // if prior setting is same as DST setting, do nothing
  if(hr != 2) return;                           // do nothing until 2pm

  DateTime now = RTC.now();                     // get time
  if(IsDST() && !dst) {
    DateTime newTime (now.unixtime() + 3600);   // add one hour
    RTC.adjust(newTime);                        
  }
  if(!IsDST() && dst) {
    DateTime newTime (now.unixtime() - 3600);   // subtract one hour
    RTC.adjust(newTime);
  }
  dst = IsDST(); saveSettings();                // save change to DST
}

// EEPROM save, restore and set/save defaults
void saveSettings()    { for(int addr = 0; addr < sizeof(set)/2; addr++) { EEPROM.write(addr, *set[addr]); } }    // save
void restoreSettings() { for(int addr = 0; addr < sizeof(set)/2; addr++) { *set[addr] = EEPROM.read(addr); } }   // recover EEPROM value

[/codesyntax]

 

Wishing you Fair Weather!

Come on... leave a comment...