/*
News, Time and Weather: ESP 8266 co-processor for the Matrix Clock series
Responds to wired interrupt and takes a series of commands to return
time, NYT RSS Feed headline and Accuweather current weather and forecast
Requires SSID, password, offset from GMT and zip code
8-25-20 Update 'remove HTML apostrophe' code and edit degree sign
8-31-20 Remove yet another HTML apostrophe sneaking in
9-16-20 Add NPR news feed read and internal test code
9-17-20 Debugged version
9-28-20 Added 'disconnect from prior network' before wifi begins
9-29-20 Added tomorrow forecast to ACW updates
10-22-20 No change
11-30-20 Reduce time between segment sends from 400 ms to 40 ms
1-5-21 Remove redundant time validity test
01-29-21 Replace '&' with just '&' in strings
*/
#define USING_AXTLS
#include <ESP8266WiFi.h>
#include <EasyNTPClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <Wire.h>
// force use of AxTLS (BearSSL is now default)
#include <WiFiClientSecureAxTLS.h>
using namespace axTLS;
// #define H7912 // house 1
#define H274 //house 2
#ifdef H7912
#define STASSID "xxxx" // Your SSID and
#define STAPSK "zzzzzzzz" // Password
#define MyOffset -8 // Your offset to GMT
#define MyZipCode "98040" // Your zipcode used for localization of weather
#endif
#ifdef H274
#define STASSID "xxxxxx"
#define STAPSK "yyyyyyyyyyy"
#define MyOffset -8
#define MyZipCode "98358"
#endif
//#define test_mode // if defined code runs standalone to test readers
#define DDT // define to turn on debugging tools
void DDTv(String st, int vt) { // Print descriptor and value
#ifdef DDT
Serial.print(" ");
Serial.print(st);
Serial.print(" ");
Serial.print(vt);
#endif
}
void DDTl(String st, int vt) { // Print descriptor and value and new line
#ifdef DDT
DDTv(st, vt);
Serial.println(" ");
#endif
}
void DDTs(String st) { // Print string
#ifdef DDT
Serial.print(st);
#endif
}
void DDTt(String st) { // Print string and new line
#ifdef DDT
Serial.println(st);
#endif
}
void DDTf(String st, float fi) { // Print descriptor and floating point value and new line
#ifdef DDT
Serial.print(" ");
Serial.print(st);
Serial.print(" ");
Serial.println(fi,4);
#endif
}
void DDTsl(String st, String vt) { // Print descriptor and string value and new line
#ifdef DDT
Serial.print(st);
Serial.println(vt);
#endif
}
const char* ssid = STASSID;
const char* password = STAPSK;
WiFiUDP udp;
EasyNTPClient ntpClient(udp, "pool.ntp.org", ((MyOffset * 60 * 60))); // initialize and set up offset
unsigned long t_unix_date;
int MyYear; // to receive the parsed date, time
int MyMonth;
int MyDay;
int MyHour;
int MyMinute;
int MySecond;
int MyWeekDay;
char MyDaysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
#define SDA 4 // D2.
#define SCL 5 // D1
const int MasterPin = 2; // D4 // Pin to take interrupt on when asked for time and date
bool ESPvalue; // Value to read interrupt status into
bool Setflag = false; // Single flag set to 'true' when interrupt occurs
const int ConnectTries = 20; // number of tries to wait for Wifi connection
const int ConnectWait = 500; // milliseconds between tries
const int WaitingTime = 2000; // milliseconds to fully delay between network status tries
const int WaitingCounter = 50; // times to wait
const int MasterChannel = 8; // although it seems like the master, to us its the slave (its I2C address)
byte Master_Command = 0;
const int MC_GetTime = 1; // potential values for clock to ask for
const int MC_GetNYT = 2; // NYT RSS
const int MC_GetACW = 3; // Current Accuweather
const int MC_GetNPR = 4; // NPR
const int MC_Bad = 0;
const int MC_Null = 5;
String line = " "; //150
String NYTLine = line;
String ACWLine = line;
String NPRLine = line;
int NYTLength; // length of returned headline
int ACWLength; // length of returned accuweather line
int NPRLength; // length of returned NPR line
bool GotNYT = false; // set true by successful "GET"
bool GotACW = false; // same - only need one for both currrent and forecast
bool GotNPR = false; // same
int Status_Flag; // result of wire transmission
int Inptr; // 'from' string pointer
int Outptr; // 'to'
const int MaxBytes = 10; // I2C has a 32 byte limitation, but I can't get past 10 or so
int NYTCCounter; // decrementing character counter
int ACWCCounter; // decrementing character counter
int NPRCCounter; // decrementing character counter
char Segment [MaxBytes]; // segment header only in first segment:
int Segment_Delay = 40; // milliseconds to wait between segment sends
const int SSH_Type = 0; // segment header 'type' (will be MC_Command parroted back)
const int SSH_Length = 1; // length of data (not including header)
const int SSH_Contents = 2; // where the data lives
const int Repeat_Sends = 5; // number of times to try to send segment
WiFiClientSecure client;
void setup() {
Serial.begin(9600);
WiFi.disconnect(); // Disconnect from any prior network
Wire.begin(SDA, SCL); // join i2c bus with address defined (int sda, int scl);
pinMode(MasterPin, INPUT_PULLUP); // initialize pin to listen to master on
// Serial.print("connecting to ");
//Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print("."); }
// Serial.println("Initially connected");
// Serial.println("");
// Serial.println("WiFi connected");
// Serial.println("IP address: ");
// Serial.println(WiFi.localIP());
#ifndef test_mode
GetCommandByte(); // ask for first byte
#endif
// DDTt("asking for initial byte");
attachInterrupt(digitalPinToInterrupt(MasterPin), ISR, FALLING ); // set to interrupt when becomes LOW
} // end of setup
void loop() {
if (WiFi.status() != WL_CONNECTED){ // if disconnected from network, restart
// DDTt("Not connected at top of loop");
ESP.restart();}
#ifdef test_mode
Setflag = true; // if testing, we are always set
#endif
if (Setflag) { // we got a wired interrupt
#ifdef test_mode // if testing a reader
Master_Command = MC_GetNPR; // THIS is the reader to test - change to suit test
#else
GetCommandByte(); // ask Master for a command byte
DDTl("Command Byte = ",Master_Command);
#endif
switch (Master_Command) {
case MC_Bad:
// DDTs("Got a zero command");
break;
case MC_GetTime:
if (!GetTheTime()) DDTt("GetThetime failed");
break;
case MC_GetNYT:
GotNYT = GetNYT(); // do the fetch from RSS site and store success/fail
if (!GotNYT) {DDTt("GETNYT failed");}
if (!SendNYT()) DDTt("SendNYT failed");
break;
case MC_GetACW:
GotACW = GetACW(); // do the fetch from RSS site and store success/fail
if (!GotACW) {DDTt("GETACW failed");}
if(!SendACW()) DDTt("SendAccuweather failed");
break;
case MC_GetNPR:
GotNPR = GetNPR(); // do the fetch from RSS site and store success/fail
if (!GotNPR) {DDTt("GETNPR failed");}
if(!SendNPR()) DDTt("SendNPR failed");
break;
case MC_Null:
DDTt("Got idle startup command");
break;
default:
DDTl("Never on CASE",Master_Command);
break;
} // end of case switch
Setflag = false; // and clear "got interrupt" flag
}
#ifdef test_mode
delay(30000); //delay between repeated reads
#endif
} // end of Void Loop
ICACHE_RAM_ATTR void ISR(){ // Simple routine to set 'true' on interrupt
Setflag = true;
// DDTt("Got interrupt from Master");
} // end of ISR
void GetCommandByte(){ // routine to ask for one byte from master clock
Wire.requestFrom (MasterChannel, 1);
if (Wire.available() == 1) {Master_Command = Wire.read();
// DDTv("Master Command received = ",Master_Command);
} else
{Master_Command = MC_Bad;
DDTv("Bad or no Command received = ",Master_Command);
DDTv("and length was",Wire.available());
}
} // end of get Command byte
bool GetNYT(){
// Use WiFiClientSecure class to create TLS connection
const char* host = "rss.nytimes.com";
const int httpsPort = 443;
// Use web browser to view and copy
// SHA1 fingerprint of the certificate
//https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml
//CB:29:78:50:52:F1:B9:1E:53:0C:BE:54:6C:11:DF:E6:29:94:D7:6E
//const char* fingerprint = "CB 29 78 50 52 F1 B9 1E 53 0C BE 54 6C 11 DF E6 29 94 D7 6E";
if (!client.connect(host, httpsPort)) {
DDTt("connection failed");
return false;}
// if (!client.verify(fingerprint, host)) { // it appears fingerprint not needed
// DDTt("certificate doesn't match");
// return false;}
String url = "/services/xml/rss/nyt/HomePage.xml";
// Serial.print("requesting URL: ");
// Serial.println(url);
client.print(String("GET ") + url +
" HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
// Serial.println("request sent");
while (client.connected()) {
line = client.readStringUntil('\n');
if (line == "\r") {
// Serial.println("headers received");
break;
}
}
line = client.readStringUntil('\n');
if (line.startsWith("<?xml version")) {
// Serial.println("Successfull!");
} else {
// Serial.println("Failed");
return false;
}
// Serial.println("reply was:");
// Serial.println("==========");
// Serial.println(line);
// Serial.println("==========");
SearchTag("<item>"); // search for item tag
line = client.readStringUntil('\n'); // read next line
RemoveTags();
line.replace("'", "'"); // another instance of HTML apostrophe sneaking in - replace it
line.replace("&", "&"); // Replace '&' with just '&'
Serial.println(line);
NYTLength = line.length();
DDTl("Length of Line is ",NYTLength);
NYTCCounter = NYTLength; // initialize the counter (send the terminator too)
line.setCharAt(NYTCCounter,'\n'); // and ensure it IS a terminator
NYTLine = line; // and copy the line for send
// Serial.println("closing connection");
return true;
} // end of GetNYT
bool SendNYT(){
if (!GotNYT){NYTLine = "NY Times Not Available"; // if didn't get it, can't send it
NYTCCounter = NYTLine.length();}
// DDTl("SendNYT Called with data length of ",NYTCCounter);
Segment[SSH_Type] = MC_GetNYT; // first byte is a flag (command mirrored)
Segment[SSH_Length] = NYTCCounter; // second is number of data bytes past header
Inptr = 0; // initialize 'get' pointer
Outptr = SSH_Contents; // and the output pointer past header bytes
while (NYTCCounter > 0){
Segment[Outptr] = NYTLine.charAt(Inptr); // copy the character
if (Segment[Outptr] == 226){ // if we hit the dreaded HTML apostrophe [sequence is 226,128,153]
Segment[Outptr] = 39; // set it to correct one
Inptr = Inptr+2; // skip next two character
NYTCCounter = NYTCCounter-2; // increment pointer, decrement count
}
// DDTv("Character",char(Segment[Outptr]));
// DDTl(" and value is ",int(Segment[Outptr]));
Inptr++; // increment in pointer and output
Outptr++;
NYTCCounter--; // and decrement remaining character
if (Outptr == MaxBytes) {
if (!Send_Segment(MaxBytes)) return false;
Outptr = 0; // and reset pointer
// DDTl("Status of NYT transmission",Status_Flag); // 0 = success, 1 = data too long, 2 NACK on address, 3= NACK on data, 4= other error*/
} // end of transmit segment
} // end of while
if (Outptr != 0){ // if we have a partial segment
if (!Send_Segment(Outptr)) return false;
// DDTl("Status of NYT stub transmission",Status_Flag); // 0 = success, 1 = data too long, 2 NACK on address, 3= NACK on data, 4= other error*/
}
return true;
} // end of SendNYT
bool GetNPR(){ // NPR News Feed
// Use WiFiClientSecure class to create TLS connection
const char* host = "feeds.npr.org";
// https://feeds.npr.org/1001/rss.xml
const int httpsPort = 443;
//const char* fingerprint = "b13ec36903f8bf4701d498261a0802ef63642bc3";
if (!client.connect(host, httpsPort)) {
DDTt("connection failed");
return false;}
// if (!client.verify(fingerprint, host)) { // does not appear to need fingerprint
// DDTt("certificate doesn't match");
// return false;}
String url = "/1001/rss.xml";
Serial.print("requesting URL: ");
Serial.println(url);
client.print(String("GET ") + url +
" HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
// Serial.println("request sent");
while (client.connected()) {
line = client.readStringUntil('\n');
if (line == "\r") {
Serial.println("headers received");
break; }
}
line = client.readStringUntil('\n');
if (line.startsWith("<?xml version")) {
// Serial.println("Successfull!");
} else {
Serial.println("Failed");
return false;
}
// Serial.println("reply was:");
// Serial.println("==========");
// Serial.println(line);
// Serial.println("==========");
SearchTag("<item>"); // search for item tag
line = client.readStringUntil('\n'); // read next line
RemoveTags();
line.replace("'", "'"); // another instance of HTML apostrophe sneaking in - replace it
line.replace("&", "&"); // Replace '&' with just '&'
Serial.println(line);
NPRLength = line.length();
DDTl("Length of Line is ",NPRLength);
NPRCCounter = NPRLength; // initialize the counter (send the terminator too)
line.setCharAt(NPRCCounter,'\n'); // and ensure it IS a terminator
NPRLine = line; // and copy the line for send
// Serial.println("closing connection");
return true;
} // end of GetNPR
bool SendNPR(){
if (!GotNPR){NPRLine = "NPR Not Available"; // if didn't get it, can't send it
NPRCCounter = NPRLine.length();}
// DDTl("SendNPR Called with data length of ",NPRCCounter);
Segment[SSH_Type] = MC_GetNPR; // first byte is a flag (command mirrored)
Segment[SSH_Length] = NPRCCounter; // second is number of data bytes past header
Inptr = 0; // initialize 'get' pointer
Outptr = SSH_Contents; // and the output pointer past header bytes
while (NPRCCounter > 0){
Segment[Outptr] = NPRLine.charAt(Inptr); // copy the character
if (Segment[Outptr] == 226){ // if we hit the dreaded HTML apostrophe [sequence is 226,128,153]
Segment[Outptr] = 39; // set it to correct one
Inptr = Inptr+2; // skip next two character
NPRCCounter = NYTCCounter-2; // increment pointer, decrement count
}
// DDTv("Character",char(Segment[Outptr]));
// DDTl(" and value is ",int(Segment[Outptr]));
Inptr++; // increment in pointer and output
Outptr++;
NPRCCounter--; // and decrement remaining character
if (Outptr == MaxBytes) {
if (!Send_Segment(MaxBytes)) return false;
Outptr = 0; // and reset pointer
// DDTl("Status of NPR transmission",Status_Flag); // 0 = success, 1 = data too long, 2 NACK on address, 3= NACK on data, 4= other error*/
} // end of transmit segment
} // end of while
if (Outptr != 0){ // if we have a partial segment
if (!Send_Segment(Outptr)) return false;
// DDTl("Status of NPR stub transmission",Status_Flag); // 0 = success, 1 = data too long, 2 NACK on address, 3= NACK on data, 4= other error*/
}
return true;
} // end of SendNPR
/*
void GetTheWeather() { // vestigial code for OpenWeatherMap get the weather...
//open weather map api key
String apiKey= "a0517cca5c02ebb3b2075b89d4121d96";
//the city you want the weather for
String location= "98040,us";
WiFiClient client;
char server[] = "api.openweathermap.org";
//api.openweathermap.org/data/2.5/weather?q=London,uk&APPID=a0517cca5c02ebb3b2075b89d4121d96
//api.openweathermap.org/data/2.5/weather?zip=98040,us
//api.openweathermap.org/data/2.5/weather?q=98040,us&APPID=a0517cca5c02ebb3b2075b89d4121d96&units=imperial&mode=xml gets you xml and farenheit
Serial.println("\nStarting connection to server...");
// if you get a connection, report back via serial:
if (client.connect(server, 80)) {
Serial.println("connected to server");
// Make a HTTP request:
client.print("GET /data/2.5/weather?");
client.print("q="+location);
client.print("&appid="+apiKey);
client.print("&cnt=3");
client.println("&units=imperial");
client.println("Host: api.openweathermap.org");
client.println("Connection: close");
client.println();
} else {
Serial.println("unable to connect");
}
delay(1000);
line = "";
line = client.readStringUntil('\n');
Serial.println(line);
} // end of Get Weather
*/
bool GetACW() { // get accuweather
// Use WiFiClientSecure class to create TLS connection
const char* host = "rss.accuweather.com";
const int httpsPort = 443;
// Use web browser to view and copy
// SHA1 fingerprint of the certificate
//https://rss.accuweather.com/rss/liveweather_rss.asp?locCode=98040
//7B:E1:6E:70:F8:2F:64:52:3B:1A:71:F4:42:43:4A:1A:11:06:AC:8E
const char* fingerprint = "7B E1 6E 70 F8 2F 64 52 3B 1A 71 F4 42 43 4A 1A 11 06 AC 8E";
if (!client.connect(host, httpsPort)) {
DDTt("connection failed");
return false;}
if (!client.verify(fingerprint, host)) {
DDTt("certificate doesn't match");
return false;
}
// String url = "/rss/liveweather_rss.asp?locCode=98040";
String url = "/rss/liveweather_rss.asp?locCode=";
url = url + MyZipCode;
// Serial.print("requesting URL: ");
// Serial.println(url);
client.print(String("GET ") + url +
" HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
// Serial.println("request sent");
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r") {
// Serial.println("headers received");
break;
}
}
line = client.readStringUntil('\n');
if (line.startsWith("<?xml version")) {
// Serial.println("Successful!");
} else {
DDTsl("XML Version test failed",line);
return false;
}
// Serial.println("reply was:");
// Serial.println("==========");
// Serial.println(line);
// Serial.println("==========");
SearchTag("<item>"); // read until <item> found
line = client.readStringUntil('\n'); // read next line
RemoveTags(); // remove tags on line
Serial.println(line); // should be today's conditions terse format
SearchTag("<description>"); // find today's longer version of day forecast
RemoveTags(); // and remove tags
line.replace(" °","~"); // Edit out HTML degree in weather feed and insert tilde
Serial.println(line); // should be today's conditions verbose
ACWLength = line.length();
// DDTl("Length of Weather Line is ",ACWLength);
ACWCCounter = ACWLength; // initialize the counter (send the terminator too)
line.setCharAt(ACWCCounter,'\n'); // and ensure it IS a terminator
ACWLine = line; // and copy the line for send
//[UNCOMMENT TO GET TOMORROW FORECAST]
SearchTag("</item>"); // read string from RSS site til out of current weather
SearchTag("<item>");
SearchTag("<title>");
RemoveTags(); // get rid of the tags
Serial.println(line); // should be forecast description (e.g., "2/27/2020 FORECAST")
SearchTag("<description>");
RemoveTags(); // clean it up
RemoveIMGThroughGIF();
RemoveTags();
Serial.println(line); // should be forecast detail (e.g., ")
SearchTag("</item>");
SearchTag("<item>");
SearchTag("<title>");
RemoveTags(); // get rid of the tags
Serial.println(line); // should be forecast description (e.g., "2/27/2020 FORECAST")
SearchTag("<description>");
RemoveTags(); // clean it up
RemoveIMGThroughGIF();
RemoveTags();
Serial.println(line); // should be forecast detail (e.g., ")
ACWLine = ACWLine + "$" + line; // combine lines (Current and forecast)
ACWLength = ACWLine.length(); // reset length to reflect combined line
// DDTl("Length of Weather Forecast Line is ",ACWFLength);
ACWCCounter = ACWLength; // reset line length (send the terminator too)
// line.setCharAt(ACWCCounter,'\n'); // and ensure it IS a terminator
//Serial.println("closing connection");
return true;
} // end of GetAccuweather
bool SendACW(){ // send "Current + "$" + "Forecast"
if (!GotACW){ACWLine = "Accuweather Not Available"; // if didn't get it, can't send it
ACWCCounter = ACWLine.length();}
// DDTl("SendACW Called with data length of ",ACWCCounter+ACWFCCounter);
Segment[SSH_Type] = MC_GetACW;
Segment[SSH_Length] = ACWCCounter;
Inptr = 0;
Outptr = SSH_Contents; // and the output pointer
while (ACWCCounter > 0){
Segment[Outptr] = ACWLine.charAt(Inptr); // copy the character
Inptr++; // increment in pointer and output
Outptr++;
ACWCCounter--; // and decrement remaining character
if (Outptr == MaxBytes) {
if (!Send_Segment(MaxBytes)) return false;
Outptr = 0; // and reset pointer
// DDTl("Status of ACW transmission",Status_Flag); // 0 = success, 1 = data too long, 2 NACK on address, 3= NACK on data, 4= other error*/
} // end of transmit segment
} // end of while
if (Outptr != 0){ // if we have a partial segment
if (!Send_Segment(Outptr)) return false;
// DDTl("Status of ACW stub transmission",Status_Flag); // 0 = success, 1 = data too long, 2 NACK on address, 3= NACK on data, 4= other error*/
}
return true;
} // end of SendACW
bool GetTheTime(){
// we got a request AND we are connected
t_unix_date = ntpClient.getUnixTime();
if(t_unix_date == 0) return false;
MyYear = year(t_unix_date);
MyMonth = month(t_unix_date);
MyDay = day(t_unix_date);
MyHour = hour(t_unix_date);
MyMinute = minute(t_unix_date);
MySecond = second(t_unix_date);
MyWeekDay = weekday(t_unix_date); // note: returns 1-7, not 0-6
/*
Serial.print("Today's Date and Time: ");
Serial.print(MyDaysOfTheWeek[MyWeekDay-1]);
Serial.print(" ");
Serial.print(MyWeekDay);
Serial.print(" ");
Serial.print(MyMonth);
Serial.print("/");
Serial.print(MyDay);
Serial.print("/");
Serial.print(MyYear);
Serial.print(" ");
Serial.print(MyHour);
Serial.print(":");
if (MyMinute < 10) {
Serial.print("0");
}
Serial.print(MyMinute);
Serial.print(":");
if (MySecond < 10) {
Serial.print("0");
}
Serial.print(MySecond);
Serial.println(" ");
*/
Segment[SSH_Type] = MC_GetTime; // load segment array up
Segment[SSH_Length] = 7; // number of data bytes
Segment[SSH_Contents] = MyYear-2019; // base year is 2019, so it fits in a byte
Segment[SSH_Contents+1] = MyMonth;
Segment[SSH_Contents+2] = MyDay;
Segment[SSH_Contents+3] = MyHour;
Segment[SSH_Contents+4] = MyMinute;
Segment[SSH_Contents+5] = MySecond;
Segment[SSH_Contents+6] = MyWeekDay;
Send_Segment(9);
return true;
}
void SearchTag(String MyTag){
do { line = client.readStringUntil('\n'); // read string from RSS site until this string is found
} while (line.indexOf(MyTag) <0);
// Serial.print("Searchtag found ");
// Serial.print(MyTag);
// Serial.print(" in line ");
// Serial.println(line);
}
void RemoveTags(){
line.remove(0,(line.indexOf(">"))+1);
line.remove(line.indexOf("<"),line.indexOf(">"));
} // end of RemoveTags
void RemoveIMGThroughGIF(){
line.remove(line.indexOf("img")-4,line.indexOf("gif"));
} // end of RIG
bool Send_Segment(int bytestosend){
int trycounter = Repeat_Sends;
do {Wire.beginTransmission(MasterChannel); // Send bytes result to Master
Wire.write(Segment,bytestosend); //
// DDTl("segment transmission sent",bytestosend);
Status_Flag = Wire.endTransmission(true);
delay(Segment_Delay);} while ((Status_Flag != 0) && (trycounter-- >0)); // keep trying until good status or counter gone
if (trycounter > 0) {return true;} else {return false;}
}
Comments
Please log in or sign up to comment.