diff --git a/printermonitor/NewsApiClient.cpp b/printermonitor/NewsApiClient.cpp new file mode 100644 index 0000000..72a069c --- /dev/null +++ b/printermonitor/NewsApiClient.cpp @@ -0,0 +1,184 @@ +/** The MIT License (MIT) + +Copyright (c) 2018 David Payne + +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 "NewsApiClient.h" + + + +#define arr_len( x ) ( sizeof( x ) / sizeof( *x ) ) + +NewsApiClient::NewsApiClient(String ApiKey, String NewsSource) { + updateNewsClient(ApiKey, NewsSource); +} + +void NewsApiClient::updateNewsClient(String ApiKey, String NewsSource) { + mySource = NewsSource; + myApiKey = ApiKey; +} + +void NewsApiClient::updateNews() { + JsonStreamingParser parser; + parser.setListener(this); + HTTPClient http; + + String apiGetData = "http://" + String(servername) + "/v2/top-headlines?sources=" + mySource + "&apiKey=" + myApiKey; + //String apiGetData = "http://" + String(servername) + "/v2/top-headlines?country=nl&apiKey=" + myApiKey; + + Serial.println("Getting News Data"); + Serial.println(apiGetData); + http.begin(apiGetData); + int httpCode = http.GET(); + + if (httpCode > 0) { // checks for connection + Serial.printf("[HTTP] GET... code: %d\n", httpCode); + if(httpCode == HTTP_CODE_OK) { + // get lenght of document (is -1 when Server sends no Content-Length header) + int len = http.getSize(); + // create buffer for read + char buff[128] = { 0 }; + // get tcp stream + WiFiClient * stream = http.getStreamPtr(); + // read all data from server + Serial.println("Start parsing..."); + while(http.connected() && (len > 0 || len == -1)) { + // get available data size + size_t size = stream->available(); + if(size) { + // read up to 128 byte + int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size)); + for(int i=0;i 0) + len -= c; + } + delay(1); + } + } + http.end(); + } else { + Serial.println("connection for news data failed: " + String(apiGetData)); //error message if no client connect + Serial.println(); + return; + } +} + +String NewsApiClient::getTitle(int index) { + return news[index].title; +} + +String NewsApiClient::getDescription(int index) { + return news[index].description; +} + +String NewsApiClient::getUrl(int index) { + return news[index].url; +} + +void NewsApiClient::updateNewsSource(String source) { + mySource = source; +} + +void NewsApiClient::whitespace(char c) { + +} + +void NewsApiClient::startDocument() { + counterTitle = 0; +} + +void NewsApiClient::key(String key) { + currentKey = key; +} + +void NewsApiClient::value(String value) { + if (counterTitle == 10) { + // we are full so return + return; + } + if (currentKey == "title") { + news[counterTitle].title = cleanText(value); + } + if (currentKey == "description") { + news[counterTitle].description = cleanText(value); + } + if (currentKey == "url") { + news[counterTitle].url = value; + counterTitle++; + } + Serial.println(currentKey + "=" + value); +} + +void NewsApiClient::endArray() { +} + +void NewsApiClient::endObject() { +} +void NewsApiClient::startArray() { +} + +void NewsApiClient::startObject() { +} + +void NewsApiClient::endDocument() { +} + +String NewsApiClient::cleanText(String text) { + text.replace("’", "'"); + text.replace("“", "\""); + text.replace("”", "\""); + text.replace("`", "'"); + text.replace("‘", "'"); + text.replace("\\\"", "'"); + text.replace("•", "-"); + text.replace("é", "e"); + text.replace("è", "e"); + text.replace("ë", "e"); + text.replace("ê", "e"); + text.replace("à", "a"); + text.replace("â", "a"); + text.replace("ù", "u"); + text.replace("ç", "c"); + text.replace("î", "i"); + text.replace("ï", "i"); + text.replace("ô", "o"); + text.replace("…", "..."); + text.replace("–", "-"); + text.replace("Â", "A"); + text.replace("À", "A"); + text.replace("æ", "ae"); + text.replace("Æ", "AE"); + text.replace("É", "E"); + text.replace("È", "E"); + text.replace("Ë", "E"); + text.replace("Ô", "O"); + text.replace("Ö", "O"); + text.replace("œ", "oe"); + text.replace("Œ", "OE"); + text.replace("Ù", "U"); + text.replace("Û", "U"); + text.replace("Ü", "U"); + return text; +} diff --git a/printermonitor/NewsApiClient.h b/printermonitor/NewsApiClient.h new file mode 100644 index 0000000..e99e589 --- /dev/null +++ b/printermonitor/NewsApiClient.h @@ -0,0 +1,70 @@ +/** The MIT License (MIT) + +Copyright (c) 2018 David Payne + +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. +*/ + +#pragma once +#include +#include +#include +#include + +class NewsApiClient: public JsonListener { + + private: + String mySource = ""; + String myApiKey = ""; + + String currentKey = ""; + int counterTitle = 0; + + typedef struct { + String title; + String description; + String url; + } newsfeed; + + newsfeed news[10]; + + const char* servername = "newsapi.org"; // remote server we will connect to + + public: + NewsApiClient(String ApiKey, String NewsSource); + void updateNewsClient(String ApiKey, String NewsSource); + void updateNews(); + void updateNewsSource(String source); + + String getTitle(int index); + String getDescription(int index); + String getUrl(int index); + String cleanText(String text); + + virtual void whitespace(char c); + virtual void startDocument(); + virtual void key(String key); + virtual void value(String value); + virtual void endArray(); + virtual void endObject(); + virtual void endDocument(); + virtual void startArray(); + virtual void startObject(); + +}; diff --git a/printermonitor/Settings.h b/printermonitor/Settings.h index 5cd795c..2980709 100644 --- a/printermonitor/Settings.h +++ b/printermonitor/Settings.h @@ -48,6 +48,7 @@ SOFTWARE. #include "SH1106Wire.h" #include "SSD1306Wire.h" #include "OLEDDisplayUi.h" +#include "NewsApiClient.h" //****************************** // Start Settings @@ -77,6 +78,10 @@ boolean IS_24HOUR = false; // 23:00 millitary 24 hour clock int minutesBetweenDataRefresh = 15; boolean DISPLAYCLOCK = true; // true = Show Clock when not printing / false = turn off display when not printing +boolean NEWS_ENABLED = true; +String NEWS_API_KEY = ""; // Get your News API Key from https://newsapi.org +String NEWS_SOURCE = "rtl-nieuws"; // https://newsapi.org/sources to get full list of news sources available + // Display Settings const int I2C_DISPLAY_ADDRESS = 0x3c; // I2C Address of your Display (usually 0x3c or 0x3d) const int SDA_PIN = D2; @@ -90,4 +95,4 @@ String OTA_Password = ""; // Set an OTA password here -- leave blank if you // End Settings //****************************** -String themeColor = "light-green"; // this can be changed later in the web interface. +String themeColor = "light-green"; // this can be changed later in the web interface. diff --git a/printermonitor/printermonitor.ino b/printermonitor/printermonitor.ino index 9914324..f8e4916 100644 --- a/printermonitor/printermonitor.ino +++ b/printermonitor/printermonitor.ino @@ -66,7 +66,7 @@ void drawClockHeaderOverlay(OLEDDisplay *display, OLEDDisplayUiState* state); // Set the number of Frames supported const int numberOfFrames = 3; FrameCallback frames[numberOfFrames]; -FrameCallback clockFrame[2]; +FrameCallback clockFrame[20]; boolean isClockOn = false; OverlayCallback overlays[] = { drawHeaderOverlay }; @@ -90,6 +90,32 @@ int printerCount = 0; // Weather Client OpenWeatherMapClient weatherClient(WeatherApiKey, CityIDs, 1, IS_METRIC); +// News Client +NewsApiClient newsClient(NEWS_API_KEY, NEWS_SOURCE); +int newsIndex = 0; + +String NEWS_OPTIONS = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + //declairing prototypes void configModeCallback (WiFiManager *myWiFiManager); int8_t getWifiQuality(); @@ -99,6 +125,7 @@ ESP8266WebServer server(WEBSERVER_PORT); String WEB_ACTIONS = " Home" " Configure" " Weather" + " News" " Reset Settings" " Forget WiFi" " About"; @@ -260,6 +287,8 @@ void setup() { server.on("/updateweatherconfig", handleUpdateWeather); server.on("/configure", handleConfigure); server.on("/configureweather", handleWeatherConfigure); + server.on("/updatenewsconfig", handleUpdateNews); + server.on("/configurenews", handleNewsConfigure); server.onNotFound(redirectHome); // Start the server server.begin(); @@ -364,6 +393,11 @@ void getUpdateTime() { weatherClient.updateWeather(); } + if (displayOn && NEWS_ENABLED) { + Serial.println("Getting News Data for " + NEWS_SOURCE); + newsClient.updateNews(); + } + Serial.println("Updating Time..."); //Update the Time timeClient.updateTime(); @@ -478,6 +512,53 @@ void handleWeatherConfigure() { digitalWrite(externalLight, HIGH); } +void handleNewsConfigure() { + if (!server.authenticate(www_username, www_password)) { + return server.requestAuthentication(); + } + digitalWrite(externalLight, LOW); + String html = ""; + + String NEWS_FORM1 = "

News Configuration:

" + "

Display News Headlines

" + "" + "" + "

Select News Source

" + "
"; + + server.sendHeader("Cache-Control", "no-cache, no-store"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send(200, "text/html", ""); + + html = getHeader(); + server.sendContent(html); + + String form = NEWS_FORM1; + String isNewsDisplayedChecked = ""; + if (NEWS_ENABLED) { + isNewsDisplayedChecked = "checked='checked'"; + } + form.replace("%NEWSCHECKED%", isNewsDisplayedChecked); + form.replace("%NEWSKEY%", NEWS_API_KEY); + server.sendContent(form); //Send first Chunk of form + String newsOptions = NEWS_OPTIONS; + newsOptions.replace(">" + NEWS_SOURCE + "<", " selected>" + NEWS_SOURCE + "<"); + server.sendContent(newsOptions); + server.sendContent(NEWS_FORM2); + + html = getFooter(); + server.sendContent(html); + + server.sendContent(""); + server.client().stop(); + digitalWrite(externalLight, HIGH); +} + + void handleConfigure() { if (!server.authenticate(www_username, www_password)) { return server.requestAuthentication(); @@ -531,6 +612,28 @@ void handleConfigure() { digitalWrite(externalLight, HIGH); } + +void handleUpdateNews() { + if (!server.authenticate(www_username, www_password)) { + return server.requestAuthentication(); + } + NEWS_ENABLED = server.hasArg("displaynews"); + NEWS_API_KEY = server.arg("newsApiKey"); + NEWS_SOURCE = server.arg("newssource"); + + writeSettings(); + isClockOn = false; // this will force a check for the display + checkDisplay(); + + if (NEWS_ENABLED) + { + newsClient.updateNews(); + } + redirectHome(); +} + + + void displayMessage(String message) { digitalWrite(externalLight, LOW); @@ -695,6 +798,25 @@ void displayPrinterStatus() { html += "

"; } + if (NEWS_ENABLED) { + if (NEWS_API_KEY == "" || NEWS_SOURCE == "") { + html += "

Please Configure News API and News Source

"; + } else { + if (newsClient.getTitle(0) == "") { + html += "

Error: No news headlines found

"; + } else { + html += "

" + NEWS_SOURCE + "

"; + html += "

"; + html += "

"; + html += "

"; + } + } + } server.sendContent(html); // spit out what we got html = ""; // fresh start } @@ -805,10 +927,90 @@ void drawWeather(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int display->setFont(ArialMT_Plain_16); display->drawString(0 + x, 24 + y, weatherClient.getCondition(0)); - display->setFont((const uint8_t*)Meteocons_Plain_42); + display->setFont(Meteocons_Plain_42); display->drawString(86 + x, 0 + y, weatherClient.getWeatherIcon(0)); } +void drawNews(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y, int newsIndex) +{ + display->setFont(ArialMT_Plain_10); + display->setTextAlignment(TEXT_ALIGN_CENTER); + //display->drawStringMaxWidth(64 + x, 0, 128, newsClient.getTitle(newsIndex)); + + // Based on the drawStringMaxWidth function of the SSD1306Wire module + char text[100]; + newsClient.getTitle(newsIndex).toCharArray(text, 99); + uint16_t length = strlen(text); + uint16_t lastDrawnPos = 0; + uint16_t lineNumber = 0; + uint16_t strWidth = 0; + uint16_t preferredBreakpoint = 0; + uint16_t widthAtBreakpoint = 0; + + for (uint16_t i = 0; i < length && lineNumber < 4 ; i++) { + strWidth += display->getStringWidth(&text[i], 1); + + // Always try to break on a space or dash + if (text[i] == ' ' || text[i]== '-') { + preferredBreakpoint = i; + widthAtBreakpoint = strWidth; + } + + if (strWidth >= 128) { + if (preferredBreakpoint == 0) { + preferredBreakpoint = i; + widthAtBreakpoint = strWidth; + } + text[preferredBreakpoint] = '\0'; + display->drawString(64 + x, y + ((lineNumber++) * 11) , &text[lastDrawnPos]); + lastDrawnPos = preferredBreakpoint + 1; + // It is possible that we did not draw all letters to i so we need + // to account for the width of the chars from `i - preferredBreakpoint` + // by calculating the width we did not draw yet. + strWidth = strWidth - widthAtBreakpoint; + preferredBreakpoint = 0; + } + } + + // Draw last part if needed + if (lastDrawnPos < length && lineNumber < 4) { + display->drawString(64 + x, y + (lineNumber * 11) , &text[lastDrawnPos]); + } +} + +// It look silly to have then function which do the same but i did not find a way to pass the news index +// trough the frame function +void drawNewsPage1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 0); +} +void drawNewsPage2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 1); +} +void drawNewsPage3(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 2); +} +void drawNewsPage4(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 3); +} +void drawNewsPage5(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 4); +} +void drawNewsPage6(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 5); +} +void drawNewsPage7(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 6); +} +void drawNewsPage8(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 7); +} +void drawNewsPage9(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 8); +} +void drawNewsPage10(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) { + drawNews(display, state, x, y, 9); +} + String getTempSymbol() { return getTempSymbol(false); } @@ -941,6 +1143,9 @@ void writeSettings() { f.println("weatherKey=" + WeatherApiKey); f.println("CityID=" + String(CityIDs[0])); f.println("isMetric=" + String(IS_METRIC)); + f.println("displaynews=" + String(NEWS_ENABLED)); + f.println("newsApiKey=" + String(NEWS_API_KEY)); + f.println("newssource=" + String(NEWS_SOURCE)); } f.close(); readSettings(); @@ -1037,6 +1242,20 @@ void readSettings() { IS_METRIC = line.substring(line.lastIndexOf("isMetric=") + 9).toInt(); Serial.println("IS_METRIC=" + String(IS_METRIC)); } + if (line.indexOf("displaynews=") >= 0) { + NEWS_ENABLED = line.substring(line.lastIndexOf("displaynews=") + 12).toInt(); + Serial.println("NEWS_ENABLED=" + String(NEWS_ENABLED)); + } + if (line.indexOf("newsApiKey=") >= 0) { + NEWS_API_KEY = line.substring(line.lastIndexOf("newsApiKey=") + 11); + NEWS_API_KEY.trim(); + Serial.println("NEWS_API_KEY=" + NEWS_API_KEY); + } + if (line.indexOf("newssource=") >= 0) { + NEWS_SOURCE = line.substring(line.lastIndexOf("newssource=") + 11); + NEWS_SOURCE.trim(); + Serial.println("NEWS_API_KEY=" + NEWS_SOURCE); + } } fr.close(); printerClient.updateOctoPrintClient(OctoPrintApiKey, OctoPrintServer, OctoPrintPort, OctoAuthUser, OctoAuthPass); @@ -1044,6 +1263,7 @@ void readSettings() { weatherClient.setMetric(IS_METRIC); weatherClient.updateCityIdList(CityIDs, 1); timeClient.setUtcOffset(UtcOffset); + newsClient.updateNewsClient(NEWS_API_KEY, NEWS_SOURCE); } int getMinutesFromLastRefresh() { @@ -1096,11 +1316,34 @@ void checkDisplay() { ui.disableAutoTransition(); ui.setFrames(clockFrame, 1); clockFrame[0] = drawClock; - } else { - ui.enableAutoTransition(); - ui.setFrames(clockFrame, 2); - clockFrame[0] = drawClock; - clockFrame[1] = drawWeather; + } + else { + if (NEWS_ENABLED) { + ui.enableAutoTransition(); + ui.setFrames(clockFrame, 13); + clockFrame[0] = drawClock; + clockFrame[1] = drawNewsPage1; + clockFrame[2] = drawNewsPage2; + clockFrame[3] = drawNewsPage3; + if (DISPLAYWEATHER) { + clockFrame[4] = drawWeather; + } else { + clockFrame[4] = drawClock; + } + clockFrame[5] = drawNewsPage4; + clockFrame[6] = drawNewsPage5; + clockFrame[7] = drawNewsPage6; + clockFrame[8] = drawClock; + clockFrame[9] = drawNewsPage7; + clockFrame[10] = drawNewsPage8; + clockFrame[11] = drawNewsPage9; + clockFrame[12] = drawNewsPage10; + } else { + ui.enableAutoTransition(); + ui.setFrames(clockFrame, 2); + clockFrame[0] = drawClock; + clockFrame[1] = drawWeather; + } } ui.setOverlays(clockOverlay, numberOfOverlays); isClockOn = true; @@ -1129,4 +1372,4 @@ void enableDisplay(boolean enable) { Serial.println("Display was turned OFF: " + timeClient.getFormattedTime()); displayOffEpoch = lastEpoch; } -} +}