diff --git a/README.md b/README.md
index dec05d0..94f7183 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ SOFTWARE.
* Configured through Web Interface
* Display 3D print progress from your OctoPrint Server
* Option to display Bitcoin current value
+* Option to display Pi-hole status and graph (each pixel accross is 10 minutes)
* Basic Authorization around Configuration web interface
* Support for OTA (loading firmware over WiFi)
* Update firmware through web interface
diff --git a/marquee.ino.d1_mini_2.11.bin b/marquee.ino.d1_mini_2.11.bin
deleted file mode 100644
index 9bbb005..0000000
Binary files a/marquee.ino.d1_mini_2.11.bin and /dev/null differ
diff --git a/marquee.ino.d1_mini_2.12.bin b/marquee.ino.d1_mini_2.12.bin
new file mode 100644
index 0000000..87cdccd
Binary files /dev/null and b/marquee.ino.d1_mini_2.12.bin differ
diff --git a/marquee.ino.d1_mini_wide_2.11.bin b/marquee.ino.d1_mini_wide_2.11.bin
deleted file mode 100644
index cfa898d..0000000
Binary files a/marquee.ino.d1_mini_wide_2.11.bin and /dev/null differ
diff --git a/marquee.ino.d1_mini_wide_2.12.bin b/marquee.ino.d1_mini_wide_2.12.bin
new file mode 100644
index 0000000..25b3a5d
Binary files /dev/null and b/marquee.ino.d1_mini_wide_2.12.bin differ
diff --git a/marquee/PiHoleClient.cpp b/marquee/PiHoleClient.cpp
new file mode 100644
index 0000000..d5cb355
--- /dev/null
+++ b/marquee/PiHoleClient.cpp
@@ -0,0 +1,300 @@
+/** 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 "PiHoleClient.h"
+
+PiHoleClient::PiHoleClient() {
+ //Constructor
+}
+
+void PiHoleClient::getPiHoleData(String server, int port) {
+
+ errorMessage = "";
+ String response = "";
+
+ String apiGetData = "http://" + server + ":" + String(port) + "/admin/api.php?summary";
+ Serial.println("Sending: " + apiGetData);
+ HTTPClient http; //Object of class HTTPClient
+ http.begin(apiGetData);// get the result
+ int httpCode = http.GET();
+ //Check the returning code
+ if (httpCode > 0) {
+ response = http.getString();
+ http.end(); //Close connection
+ if (httpCode != 200) {
+ // Bad Response Code
+ errorMessage = "Error response (" + String(httpCode) + "): " + response;
+ Serial.println(errorMessage);
+ return;
+ }
+ Serial.println("Response Code: " + String(httpCode));
+ //Serial.println("Response: " + response);
+ } else {
+ errorMessage = "Failed to connect and get data: " + apiGetData;
+ Serial.println(errorMessage);
+ return;
+ }
+
+ const size_t bufferSize = 2*JSON_OBJECT_SIZE(3) + JSON_OBJECT_SIZE(17) + 470;
+ DynamicJsonBuffer jsonBuffer(bufferSize);
+
+ // Parse JSON object
+ JsonObject& root = jsonBuffer.parseObject(response);
+ if (!root.success()) {
+ errorMessage = "Data Summary Parsing failed: " + apiGetData;
+ Serial.println(errorMessage);
+ return;
+ }
+
+ piHoleData.domains_being_blocked = (const char*)root["domains_being_blocked"];
+ piHoleData.dns_queries_today = (const char*)root["dns_queries_today"];
+ piHoleData.ads_blocked_today = (const char*)root["ads_blocked_today"];
+ piHoleData.ads_percentage_today = (const char*)root["ads_percentage_today"];
+ piHoleData.unique_domains = (const char*)root["unique_domains"];
+ piHoleData.queries_forwarded = (const char*)root["queries_forwarded"];
+ piHoleData.queries_cached = (const char*)root["queries_cached"];
+ piHoleData.clients_ever_seen = (const char*)root["clients_ever_seen"];
+ piHoleData.unique_clients = (const char*)root["unique_clients"];
+ piHoleData.dns_queries_all_types = (const char*)root["dns_queries_all_types"];
+ piHoleData.reply_NODATA = (const char*)root["reply_NODATA"];
+ piHoleData.reply_NXDOMAIN = (const char*)root["reply_NXDOMAIN"];
+ piHoleData.reply_CNAME = (const char*)root["reply_CNAME"];
+ piHoleData.reply_IP = (const char*)root["reply_IP"];
+ piHoleData.privacy_level = (const char*)root["privacy_level"];
+ piHoleData.piHoleStatus = (const char*)root["status"];
+
+ Serial.println("Pi-Hole Status: " + piHoleData.piHoleStatus);
+ Serial.println("Todays Percentage Blocked: " + piHoleData.ads_percentage_today);
+ Serial.println();
+}
+
+void PiHoleClient::getTopClientsBlocked(String server, int port, String apiKey) {
+ errorMessage = "";
+ resetClientsBlocked();
+
+ if (apiKey == "") {
+ errorMessage = "Pi-hole API Key is required to view Top Clients Blocked.";
+ return;
+ }
+
+ String response = "";
+
+ String apiGetData = "http://" + server + ":" + String(port) + "/admin/api.php?topClientsBlocked=3&auth=" + apiKey;
+ Serial.println("Sending: " + apiGetData);
+ HTTPClient http; //Object of class HTTPClient
+ http.begin(apiGetData);// get the result
+ int httpCode = http.GET();
+ //Check the returning code
+ if (httpCode > 0) {
+ response = http.getString();
+ http.end(); //Close connection
+ if (httpCode != 200) {
+ // Bad Response Code
+ errorMessage = "Error response (" + String(httpCode) + "): " + response;
+ Serial.println(errorMessage);
+ return;
+ }
+ Serial.println("Response Code: " + String(httpCode));
+ //Serial.println("Response: " + response);
+ } else {
+ errorMessage = "Failed to get data: " + apiGetData;
+ Serial.println(errorMessage);
+ return;
+ }
+
+ const size_t bufferSize = JSON_OBJECT_SIZE(1) + JSON_OBJECT_SIZE(3) + 70;
+ DynamicJsonBuffer jsonBuffer(bufferSize);
+
+ // Parse JSON object
+ JsonObject& root = jsonBuffer.parseObject(response);
+ if (!root.success()) {
+ errorMessage = "Data Parsing failed -- verify your Pi-hole API key.";
+ Serial.println(errorMessage);
+ return;
+ }
+
+ JsonObject& blocked = root["top_sources_blocked"];
+ int count = 0;
+ for (JsonPair p : blocked) {
+ blockedClients[count].clientAddress = (const char*)p.key;
+ blockedClients[count].blockedCount = p.value.as
Use 24 Hour Clock (military time)
"; -String CHANGE_FORM2 = "Show PM indicator (only 12h format)
" +static const char CHANGE_FORM2[] PROGMEM = "Show PM indicator (only 12h format)
" "Flash : in the time
" "" "" @@ -115,13 +133,17 @@ String CHANGE_FORM2 = "Minutes Between Refresh Data
" "Minutes Between Scrolling Data
"; -String CHANGE_FORM3 = "Use Security Credentials for Configuration Changes
" +static const char CHANGE_FORM3[] PROGMEM = "Use Security Credentials for Configuration Changes
" "" "" "" ""; -String CURRENCY_OPTIONS = "" +static const char BITCOIN_FORM[] PROGMEM = ""; + +static const char CURRENCY_OPTIONS[] PROGMEM = "" "" "" "" @@ -132,6 +154,47 @@ String CURRENCY_OPTIONS = "" "" ""; +static const char WIDECLOCK_FORM[] PROGMEM = ""; + +static const char PIHOLE_FORM[] PROGMEM = "" + ""; + +static const char PIHOLE_TEST[] PROGMEM = ""; + +static const char NEWS_FORM1[] PROGMEM = ""; + +static const char OCTO_FORM[] PROGMEM = "" + ""; + + + const int TIMEOUT = 500; // 500 = 1/2 second int timeoutCount = 0; @@ -244,6 +307,7 @@ void setup() { server.on("/savewideclock", handleSaveWideClock); server.on("/savenews", handleSaveNews); server.on("/saveoctoprint", handleSaveOctoprint); + server.on("/savepihole", handleSavePihole); server.on("/systemreset", handleSystemReset); server.on("/forgetwifi", handleForgetWifi); server.on("/configure", handleConfigure); @@ -251,6 +315,7 @@ void setup() { server.on("/configurewideclock", handleWideClockConfigure); server.on("/configurenews", handleNewsConfigure); server.on("/configureoctoprint", handleOctoprintConfigure); + server.on("/configurepihole", handlePiholeConfigure); server.on("/display", handleDisplay); server.onNotFound(redirectHome); serverUpdater.setup(&server, "/update", www_username, www_password); @@ -346,8 +411,16 @@ void loop() { if (BitcoinCurrencyCode != "NONE" && BitcoinCurrencyCode != "") { msg += " Bitcoin: " + bitcoinClient.getRate() + " " + bitcoinClient.getCode() + " "; } + if (USE_PIHOLE) { + piholeClient.getPiHoleData(PiHoleServer, PiHolePort); + piholeClient.getGraphData(PiHoleServer, PiHolePort); + if (piholeClient.getPiHoleStatus() != "") { + msg += " Pi-hole (" + piholeClient.getPiHoleStatus() + "): " + piholeClient.getAdsPercentageToday() + "% "; + } + } scrollMessage(msg); + drawPiholeGraph(); } } @@ -464,6 +537,21 @@ void handleSaveOctoprint() { redirectHome(); } +void handleSavePihole() { + if (!athentication()) { + return server.requestAuthentication(); + } + USE_PIHOLE = server.hasArg("displaypihole"); + PiHoleServer = server.arg("piholeAddress"); + PiHolePort = server.arg("piholePort").toInt(); + writeCityIds(); + if (USE_PIHOLE) { + piholeClient.getPiHoleData(PiHoleServer, PiHolePort); + piholeClient.getGraphData(PiHoleServer, PiHolePort); + } + redirectHome(); +} + void handleLocations() { if (!athentication()) { return server.requestAuthentication(); @@ -537,12 +625,8 @@ void handleBitcoinConfigure() { sendHeader(); - String BITCOIN_FORM = ""; - - String form = BITCOIN_FORM; - String bitcoinOptions = CURRENCY_OPTIONS; + String form = (const char*)BITCOIN_FORM; + String bitcoinOptions = (const char*)CURRENCY_OPTIONS; bitcoinOptions.replace(BitcoinCurrencyCode + "'", BitcoinCurrencyCode + "' selected"); form.replace("%BITCOINOPTIONS%", bitcoinOptions); server.sendContent(form); //Send another Chunk of form @@ -560,10 +644,6 @@ void handleWideClockConfigure() { } digitalWrite(externalLight, LOW); - String WIDECLOCK_FORM = ""; - server.sendHeader("Cache-Control", "no-cache, no-store"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); @@ -574,7 +654,7 @@ void handleWideClockConfigure() { if (numberOfHorizontalDisplays >= 8) { // Wide display options - String form = WIDECLOCK_FORM; + String form = (const char*)WIDECLOCK_FORM; String clockOptions = ""; clockOptions.replace(Wide_Clock_Style + "'", Wide_Clock_Style + "' selected"); form.replace("%WIDECLOCKOPTIONS%", clockOptions); @@ -593,19 +673,7 @@ void handleNewsConfigure() { return server.requestAuthentication(); } digitalWrite(externalLight, LOW); - - String NEWS_FORM1 = ""; - server.sendHeader("Cache-Control", "no-cache, no-store"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); @@ -614,7 +682,7 @@ void handleNewsConfigure() { sendHeader(); - String form = NEWS_FORM1; + String form = (const char*)NEWS_FORM1; String isNewsDisplayedChecked = ""; if (NEWS_ENABLED) { isNewsDisplayedChecked = "checked='checked'"; @@ -637,17 +705,6 @@ void handleOctoprintConfigure() { } digitalWrite(externalLight, LOW); - String OCTO_FORM = "" - ""; - server.sendHeader("Cache-Control", "no-cache, no-store"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "-1"); @@ -656,7 +713,7 @@ void handleOctoprintConfigure() { sendHeader(); - String form = OCTO_FORM; + String form = (const char*)OCTO_FORM; String isOctoPrintDisplayedChecked = ""; if (OCTOPRINT_ENABLED) { isOctoPrintDisplayedChecked = "checked='checked'"; @@ -681,6 +738,42 @@ void handleOctoprintConfigure() { digitalWrite(externalLight, HIGH); } +void handlePiholeConfigure() { + if (!athentication()) { + return server.requestAuthentication(); + } + digitalWrite(externalLight, LOW); + + 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", ""); + + sendHeader(); + + String form = (const char*)PIHOLE_TEST; + server.sendContent(form); + + form = (const char*)PIHOLE_FORM; + String isPiholeDisplayedChecked = ""; + if (USE_PIHOLE) { + isPiholeDisplayedChecked = "checked='checked'"; + } + form.replace("%PIHOLECHECKED%", isPiholeDisplayedChecked); + form.replace("%PIHOLEADDRESS%", PiHoleServer); + form.replace("%PIHOLEPORT%", String(PiHolePort)); + + server.sendContent(form); + form = ""; + + sendFooter(); + + server.sendContent(""); + server.client().stop(); + digitalWrite(externalLight, HIGH); +} + void handleConfigure() { if (!athentication()) { return server.requestAuthentication(); @@ -696,7 +789,7 @@ void handleConfigure() { sendHeader(); - String form = CHANGE_FORM1; + String form = (const char*)CHANGE_FORM1; form.replace("%TIMEDBKEY%", TIMEDBKEY); form.replace("%WEATHERKEY%", APIKEY); @@ -744,7 +837,7 @@ void handleConfigure() { form.replace("%CHECKED%", checked); server.sendContent(form); - form = CHANGE_FORM2; + form = (const char*)CHANGE_FORM2; String isPmChecked = ""; if (IS_PM) { isPmChecked = "checked='checked'"; @@ -760,7 +853,7 @@ void handleConfigure() { form.replace("%ENDTIME%", timeDisplayTurnsOff); form.replace("%INTENSITYOPTIONS%", String(displayIntensity)); String dSpeed = String(displayScrollSpeed); - String scrollOptions = ""; + String scrollOptions = ""; scrollOptions.replace(dSpeed + "'", dSpeed + "' selected" ); form.replace("%SCROLLOPTIONS%", scrollOptions); String minutes = String(minutesBetweenDataRefresh); @@ -771,7 +864,7 @@ void handleConfigure() { server.sendContent(form); //Send another chunk of the form - form = CHANGE_FORM3; + form = (const char*)CHANGE_FORM3; String isUseSecurityChecked = ""; if (IS_BASIC_AUTH) { isUseSecurityChecked = "checked='checked'"; @@ -893,32 +986,18 @@ void redirectHome() { } void sendHeader() { - String WEB_ACTIONS1 = " Home" - " Configure" - " News" - " OctoPrint"; - - String WEB_ACTIONS2 = " Bitcoin" - " Refresh Data" - ""; - - String WEB_ACTION3 = " Reset Settings" - " Forget WiFi" - " Firmware Update" - " About"; - - String menu = WEB_ACTIONS1; + String menu = (const char*)WEB_ACTIONS1; if (numberOfHorizontalDisplays >= 8) { - menu += " Wide Clock"; + menu += " Wide Clock"; } - menu += WEB_ACTIONS2; + menu += (const char*)WEB_ACTIONS2; if (displayOn) { - menu += " Turn Display OFF"; + menu += " Turn Display OFF"; } else { - menu += " Turn Display ON"; + menu += " Turn Display ON"; } - menu += WEB_ACTION3; + menu += (const char*)WEB_ACTION3; String html = ""; html += "