diff --git a/.gitignore b/.gitignore index e86f0cf3..63ae6684 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ .vscode .pio platformio-user.ini -src/version.h \ No newline at end of file +/src/version.h diff --git a/addversion.py b/addversion.py new file mode 100644 index 00000000..7453a4a1 --- /dev/null +++ b/addversion.py @@ -0,0 +1,16 @@ +import os + +FILENAME_VERSION_H = 'src/version.h' +version = os.environ.get('GITHUB_REF') +if version == None: + version = "SNAPSHOT" + +import datetime + +hf = """ +#ifndef VERSION + #define VERSION "{}" +#endif +""".format(version) +with open(FILENAME_VERSION_H, 'w+') as f: + f.write(hf) diff --git a/lib/HanConfigAp/src/HanConfigAp.cpp b/lib/HanConfigAp/src/HanConfigAp.cpp index b43364cf..8cfeef96 100644 --- a/lib/HanConfigAp/src/HanConfigAp.cpp +++ b/lib/HanConfigAp/src/HanConfigAp.cpp @@ -1,14 +1,5 @@ #include "HanConfigAp.h" -#include "config_html.h" -#include "style_css.h" -#include "Base64.h" - -#if defined(ESP8266) -ESP8266WebServer HanConfigAp::server(80); -#elif defined(ESP32) // ARDUINO_ARCH_ESP32 -WebServer HanConfigAp::server(80); -#endif Stream* HanConfigAp::debugger; bool HanConfigAp::hasConfig() { @@ -73,213 +64,15 @@ void HanConfigAp::setup(int accessPointButtonPin, Stream* debugger) } } -void HanConfigAp::enableWeb() { - server.on("/", handleRoot); - server.on("/style.css", handleStyle); - server.on("/save", handleSave); - server.begin(); // Web server start - - print("Web server is ready for config at http://"); - if(isActivated) { - print(WiFi.softAPIP()); - } else { - print(WiFi.localIP()); - } - println("/"); -} - bool HanConfigAp::loop() { if(isActivated) { //DNS dnsServer.processNextRequest(); } - //HTTP - server.handleClient(); - return isActivated; } -/** Handle root or redirect to captive portal */ -void HanConfigAp::handleRoot() { - println("Serving / over http..."); - - configuration *config = new configuration(); - config->load(); - - String html = String((const __FlashStringHelper*) CONFIG_HTML); - - if(config->hasConfig()) { - bool access = !config->isAuth(); - if(config->isAuth() && server.hasHeader("Authorization")) { - String expectedAuth = String(config->authUser) + ":" + String(config->authPass); - - String providedPwd = server.header("Authorization"); - providedPwd.replace("Basic ", ""); - char inputString[providedPwd.length()]; - providedPwd.toCharArray(inputString, providedPwd.length()+1); - - int inputStringLength = sizeof(inputString); - int decodedLength = Base64.decodedLength(inputString, inputStringLength); - char decodedString[decodedLength]; - Base64.decode(decodedString, inputString, inputStringLength); - print("Received auth: "); - println(decodedString); - access = String(decodedString).equals(expectedAuth); - } - - if(!access) { - server.sendHeader("WWW-Authenticate", "Basic realm=\"Secure Area\""); - server.send(401, "text/html", ""); - } else { - server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - server.sendHeader("Pragma", "no-cache"); - server.sendHeader("Expires", "-1"); - - html.replace("${config.ssid}", config->ssid); - html.replace("${config.ssidPassword}", config->ssidPassword); - switch (config->meterType) { - case 1: - html.replace("${config.meterType0}", ""); - html.replace("${config.meterType1}", "selected"); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", ""); - break; - case 2: - html.replace("${config.meterType0}", ""); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", "selected"); - html.replace("${config.meterType3}", ""); - break; - case 3: - html.replace("${config.meterType0}", ""); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", "selected"); - break; - default: - html.replace("${config.meterType0}", "selected"); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", ""); - } - html.replace("${config.mqtt}", config->mqtt); - html.replace("${config.mqttPort}", String(config->mqttPort)); - html.replace("${config.mqttClientID}", config->mqttClientID); - html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic); - html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic); - html.replace("${config.mqttUser}", config->mqttUser); - html.replace("${config.mqttPass}", config->mqttPass); - html.replace("${config.authUser}", config->authUser); - html.replace("${config.authPass}", config->authPass); - } - - } else { - server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - server.sendHeader("Pragma", "no-cache"); - server.sendHeader("Expires", "-1"); - - html.replace("${config.ssid}", ""); - html.replace("${config.ssidPassword}", ""); - html.replace("${config.meterType0}", "selected"); - html.replace("${config.meterType1}", ""); - html.replace("${config.meterType2}", ""); - html.replace("${config.meterType3}", ""); - html.replace("${config.mqtt}", ""); - html.replace("${config.mqttPort}", "1883"); - html.replace("${config.mqttClientID}", ""); - html.replace("${config.mqttPublishTopic}", ""); - html.replace("${config.mqttSubscribeTopic}", ""); - html.replace("${config.mqttUser}", ""); - html.replace("${config.mqttPass}", ""); - html.replace("${config.authUser}", ""); - html.replace("${config.authPass}", ""); - } - server.send(200, "text/html", html); -} - -void HanConfigAp::handleStyle() { - println("Serving /style.css over http..."); - - String css = String((const __FlashStringHelper*) STYLE_CSS); - - server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); - server.sendHeader("Pragma", "no-cache"); - server.sendHeader("Expires", "-1"); - server.send(200, "text/css", css); -} - -void HanConfigAp::handleSave() { - configuration *config = new configuration(); - - String temp; - - temp = server.arg("ssid"); - config->ssid = new char[temp.length() + 1]; - temp.toCharArray(config->ssid, temp.length() + 1, 0); - - temp = server.arg("ssidPassword"); - config->ssidPassword = new char[temp.length() + 1]; - temp.toCharArray(config->ssidPassword, temp.length() + 1, 0); - - config->meterType = (byte)server.arg("meterType").toInt(); - - temp = server.arg("mqtt"); - config->mqtt = new char[temp.length() + 1]; - temp.toCharArray(config->mqtt, temp.length() + 1, 0); - - config->mqttPort = (int)server.arg("mqttPort").toInt(); - - temp = server.arg("mqttClientID"); - config->mqttClientID = new char[temp.length() + 1]; - temp.toCharArray(config->mqttClientID, temp.length() + 1, 0); - - temp = server.arg("mqttPublishTopic"); - config->mqttPublishTopic = new char[temp.length() + 1]; - temp.toCharArray(config->mqttPublishTopic, temp.length() + 1, 0); - - temp = server.arg("mqttSubscribeTopic"); - config->mqttSubscribeTopic = new char[temp.length() + 1]; - temp.toCharArray(config->mqttSubscribeTopic, temp.length() + 1, 0); - - temp = server.arg("mqttUser"); - config->mqttUser = new char[temp.length() + 1]; - temp.toCharArray(config->mqttUser, temp.length() + 1, 0); - - temp = server.arg("mqttPass"); - config->mqttPass = new char[temp.length() + 1]; - temp.toCharArray(config->mqttPass, temp.length() + 1, 0); - - temp = server.arg("authUser"); - config->authUser = new char[temp.length() + 1]; - temp.toCharArray(config->authUser, temp.length() + 1, 0); - - temp = server.arg("authPass"); - config->authPass = new char[temp.length() + 1]; - temp.toCharArray(config->authPass, temp.length() + 1, 0); - - println("Saving configuration now..."); - - if (HanConfigAp::debugger) config->print(HanConfigAp::debugger); - if (config->save()) - { - println("Successfully saved. Will reboot now."); - String html = "

Successfully Saved!

Device is restarting now...

"; - server.send(200, "text/html", html); -#if defined(ESP8266) - ESP.reset(); -#elif defined(ESP32) - ESP.restart(); -#endif - } - else - { - println("Error saving configuration"); - String html = "

Error saving configuration!

"; - server.send(500, "text/html", html); - } -} - size_t HanConfigAp::print(const char* text) { if (debugger) debugger->print(text); diff --git a/lib/HanConfigAp/src/HanConfigAp.h b/lib/HanConfigAp/src/HanConfigAp.h index d1e01d85..e62dce16 100644 --- a/lib/HanConfigAp/src/HanConfigAp.h +++ b/lib/HanConfigAp/src/HanConfigAp.h @@ -11,10 +11,8 @@ #if defined(ESP8266) #include - #include #elif defined(ESP32) // ARDUINO_ARCH_ESP32 #include - #include #else #warning "Unsupported board type" #endif @@ -27,7 +25,6 @@ class HanConfigAp { public: void setup(int accessPointButtonPin, Stream* debugger); - void enableWeb(); bool loop(); bool hasConfig(); configuration config; @@ -45,16 +42,6 @@ private: static size_t print(const Printable& data); static size_t println(const Printable& data); - // Web server - static void handleRoot(); - static void handleStyle(); - static void handleSave(); -#if defined(ESP8266) - static ESP8266WebServer server; -#elif defined(ESP32) // ARDUINO_ARCH_ESP32 - static WebServer server; -#endif - static Stream* debugger; }; diff --git a/lib/HanConfigAp/src/config_html.h b/lib/HanConfigAp/src/config_html.h deleted file mode 100644 index 310b0bce..00000000 --- a/lib/HanConfigAp/src/config_html.h +++ /dev/null @@ -1,85 +0,0 @@ -const char CONFIG_HTML[] PROGMEM = R"=="==( - - - - - - - - - AMS2MQTT - configuration - - -
-
-
-
-

WiFi

-
-
- -
-
- -
-
-
-
-

Meter Type

-
-
- -
-
-
-
-

MQTT

-
-
- - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-

Webserver

-
-
- - -
-
- - -
-
-
-
- - -)=="=="; diff --git a/lib/HanConfigAp/src/configuration.cpp b/lib/HanConfigAp/src/configuration.cpp index 3fd2e8ce..0a14ae95 100644 --- a/lib/HanConfigAp/src/configuration.cpp +++ b/lib/HanConfigAp/src/configuration.cpp @@ -39,12 +39,14 @@ bool configuration::save() address += saveBool(address, false); - address += saveBool(address, isAuth()); - if (isAuth()) { + address += saveByte(address, authSecurity); + if (authSecurity > 0) { address += saveString(address, authUser); address += saveString(address, authPass); } + address += saveInt(address, fuseSize); + bool success = EEPROM.commit(); EEPROM.end(); @@ -67,8 +69,10 @@ bool configuration::load() mqttUser = 0; mqttPass = 0; mqttPort = 1883; + authSecurity = 0; authUser = 0; authPass = 0; + fuseSize = 0; EEPROM.begin(EEPROM_SIZE); int cs = EEPROM.read(address); @@ -102,17 +106,17 @@ bool configuration::load() success = true; } if(cs >= 72) { - bool auth = false; - address += readBool(address, &auth); - if (auth) { + address += readByte(address, &authSecurity); + if (authSecurity > 0) { address += readString(address, &authUser); address += readString(address, &authPass); } else { authUser = 0; authPass = 0; } - - success = true; + } + if(cs >= 73) { + address += readInt(address, &fuseSize); } EEPROM.end(); return success; @@ -123,10 +127,6 @@ bool configuration::isSecure() return (mqttUser != 0) && (String(mqttUser).length() > 0); } -bool configuration::isAuth() { - return (authUser != 0) && (String(authUser).length() > 0); -} - int configuration::readInt(int address, int *value) { int lower = EEPROM.read(address); @@ -190,11 +190,13 @@ void configuration::print(Stream* debugger) debugger->printf("mqttPass: %s\r\n", this->mqttPass); } - if (this->isAuth()) { + if (this->authSecurity > 0) { debugger->printf("WEB AUTH:\r\n"); + debugger->printf("authSecurity: %i\r\n", this->authSecurity); debugger->printf("authUser: %s\r\n", this->authUser); debugger->printf("authPass: %s\r\n", this->authPass); } + debugger->printf("fuseSize: %i\r\n", this->fuseSize); debugger->println("-----------------------------------------------"); } diff --git a/lib/HanConfigAp/src/configuration.h b/lib/HanConfigAp/src/configuration.h index d785a094..68a142ba 100644 --- a/lib/HanConfigAp/src/configuration.h +++ b/lib/HanConfigAp/src/configuration.h @@ -25,12 +25,14 @@ public: char* mqttPass; byte meterType; + byte authSecurity; char* authUser; char* authPass; + int fuseSize; + bool hasConfig(); bool isSecure(); - bool isAuth(); bool save(); bool load(); @@ -39,7 +41,7 @@ protected: private: const int EEPROM_SIZE = 512; - const byte EEPROM_CHECK_SUM = 72; // Used to check if config is stored. Change if structure changes + const byte EEPROM_CHECK_SUM = 73; // Used to check if config is stored. Change if structure changes const int EEPROM_CONFIG_ADDRESS = 0; int saveString(int pAddress, char* pString); diff --git a/lib/HanConfigAp/src/style_css.h b/lib/HanConfigAp/src/style_css.h deleted file mode 100644 index 9b28b09c..00000000 --- a/lib/HanConfigAp/src/style_css.h +++ /dev/null @@ -1,135 +0,0 @@ -const char STYLE_CSS[] PROGMEM = R"=="==( -body,div,input { - font-family: "Roboto", Arial, Lucida Grande; -} -.wrapper { - width: 500px; - position: absolute; - padding: 30px; - background-color: #FFF; - border-radius: 1px; - color: #333; - border-color: rgba(0, 0, 0, 0.03); - box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12); - margin-left: 20px; - margin-top: 20px; -} -div { - padding-bottom: 5px; -} -label { - font-family: "Roboto", "Helvetica Neue", sans-serif; - font-size: 14px; - line-height: 16px; - width: 100px; - display: inline-block; -} -input { - font-family: "Roboto", "Helvetica Neue", sans-serif; - font-size: 14px; - line-height: 16px; - bottom: 30px; - border: none; - border-bottom: 1px solid #d4d4d4; - padding: 10px; - background: transparent; - transition: all .25s ease; -} -input[type=number] { - width: 70px; - margin-left: 5px; -} -input:focus { - outline: none; - border-bottom: 1px solid #3f51b5; -} -h2 { - text-align: left; - font-size: 20px; - font-weight: bold; - letter-spacing: 3px; - line-height: 28px; -} -.submit-button { - position: absolute; - text-align: right; - border-radius: 20px; - border-bottom-right-radius: 0; - border-top-right-radius: 0; - background-color: #3f51b5; - color: #FFF; - padding: 12px 25px; - display: inline-block; - font-size: 12px; - font-weight: bold; - letter-spacing: 2px; - right: 0px; - bottom: 10px; - cursor: pointer; - transition: all .25s ease; - box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12); - width: 100px; -} -.select-style { - border-top: 10px solid white; - border-bottom: 1px solid #d4d4d4; - color: #ffffff; - cursor: pointer; - display: block; - font-family: Roboto, "Helvetica Neue", sans-serif; - font-size: 14px; - font-weight: 400; - height: 16px; - line-height: 14px; - min-width: 200px; - padding-bottom: 7px; - padding-left: 0px; - padding-right: 0px; - position: relative; - text-align: left; - width: 80%; - -webkit-box-direction: normal; - overflow: hidden; - background: #ffffff url("data:image/png;base64,R0lGODlhDwAUAIABAAAAAP///yH5BAEAAAEALAAAAAAPABQAAAIXjI+py+0Po5wH2HsXzmw//lHiSJZmUAAAOw==") no-repeat 98% 50%; -} -.disabled-option { - color: #d4d4d4; -} -.select-style select { - padding: 5px 8px; - width: 100%; - border: none; - box-shadow: none; - background: transparent; - background-image: none; - -webkit-appearance: none; -} -.select-style select:focus { - outline: none; - border: none; -} -@media only screen and (max-width: 1000px) { - .wrapper { - width: 80%; - } -} -@media only screen and (max-width: 300px) { - .wrapper { - width: 75%; - } -} -@media only screen and (max-width: 600px) { - .wrapper { - width: 80%; - margin-left: 0px; - margin-top: 0px; - } - .submit-button { - bottom: 0px; - width: 70px; - } - input { - width: 100%; - } -} -)=="=="; diff --git a/platformio-user.ini-example b/platformio-user.ini-example index fdf27238..cd08b246 100644 --- a/platformio-user.ini-example +++ b/platformio-user.ini-example @@ -9,5 +9,6 @@ lib_deps = ${common.lib_deps} build_flags = -D HAS_DALLAS_TEMP_SENSOR=0 -D IS_CUSTOM_AMS_BOARD=0 + -D DEBUG_MODE=1 monitor_speed = 2400 monitor_flags = --parity E diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index b3279749..69ce40c6 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -22,6 +22,7 @@ #include #endif +#include "AmsWebServer.h" #include "HanConfigAp.h" #include "HanReader.h" #include "HanToJson.h" @@ -48,6 +49,8 @@ DallasTemperature tempSensor(&oneWire); // Object used to boot as Access Point HanConfigAp ap; +AmsWebServer ws; + // WiFi client and MQTT client WiFiClient *client; MQTTClient mqtt(384); @@ -102,7 +105,7 @@ void setup() { hanReader.compensateFor09HeaderBug = (ap.config.meterType == 1); } - ap.enableWeb(); + ws.setup(&ap.config, debugger); } // the loop function runs over and over again until power down or reset @@ -121,10 +124,7 @@ void loop() // Reconnect to WiFi and MQTT as needed if (!mqtt.connected()) { MQTT_connect(); - } - else - { - // Read data from the HAN port + } else { readHanPort(); } } @@ -134,6 +134,7 @@ void loop() if (millis() / 1000 % 2 == 0) led_on(); else led_off(); } + ws.loop(); } @@ -204,7 +205,7 @@ void mqttMessageReceived(String &topic, String &payload) void readHanPort() { - if (hanReader.read()) + if (hanReader.read() && ap.config.hasConfig()) { // Flash LED on, this shows us that data is received led_on(); @@ -234,16 +235,14 @@ void readHanPort() hanToJson(data, ap.config.meterType, hanReader); - // Write the json to the debug port - if (debugger) { - debugger->print("Sending data to MQTT: "); - serializeJsonPretty(json, *debugger); - debugger->println(); - } + if(ap.config.mqtt != 0 && strlen(ap.config.mqtt) != 0 && ap.config.mqttPublishTopic != 0 && strlen(ap.config.mqttPublishTopic) != 0) { + // Write the json to the debug port + if (debugger) { + debugger->print("Sending data to MQTT: "); + serializeJsonPretty(json, *debugger); + debugger->println(); + } - // Make sure we have configured a publish topic - if (! ap.config.mqttPublishTopic == 0 || strlen(ap.config.mqttPublishTopic) == 0) - { // Publish the json to the MQTT server String msg; serializeJson(json, msg); @@ -251,6 +250,7 @@ void readHanPort() mqtt.publish(ap.config.mqttPublishTopic, msg.c_str()); mqtt.loop(); } + ws.setJson(json); // Flash LED off led_off(); diff --git a/src/AmsWebServer.cpp b/src/AmsWebServer.cpp new file mode 100644 index 00000000..36abe811 --- /dev/null +++ b/src/AmsWebServer.cpp @@ -0,0 +1,367 @@ +#include "AmsWebServer.h" +#include "version.h" + +#include "index_html.h" +#include "configuration_html.h" +#include "boot_css.h" +#include "application_css.h" +#include "gaugemeter_js.h" +#include "index_js.h" + +#include "Base64.h" + +#if defined(ESP8266) +ESP8266WebServer server(80); +#elif defined(ESP32) // ARDUINO_ARCH_ESP32 +WebServer server(80); +#endif + +void AmsWebServer::setup(configuration* config, Stream* debugger) { + this->config = config; + this->debugger = debugger; + + server.on("/", std::bind(&AmsWebServer::indexHtml, this)); + server.on("/configuration", std::bind(&AmsWebServer::configurationHtml, this)); + server.on("/css/boot.css", std::bind(&AmsWebServer::bootCss, this)); + server.on("/css/application.css", std::bind(&AmsWebServer::applicationCss, this)); + server.on("/js/gaugemeter.js", std::bind(&AmsWebServer::gaugemeterJs, this)); + server.on("/js/index.js", std::bind(&AmsWebServer::indexJs, this)); + server.on("/data.json", std::bind(&AmsWebServer::dataJson, this)); + + server.on("/save", std::bind(&AmsWebServer::handleSave, this)); + + server.begin(); // Web server start + + print("Web server is ready for config at http://"); + if(WiFi.getMode() == WIFI_AP) { + print(WiFi.softAPIP()); + } else { + print(WiFi.localIP()); + } + println("/"); + + if(config->hasConfig() && config->fuseSize > 0) { + maxPwr = config->fuseSize * 230; + } else { + maxPwr = 20000; + } +} + +void AmsWebServer::loop() { + server.handleClient(); +} + +void AmsWebServer::setJson(StaticJsonDocument<500> json) { + if(!json.isNull()) { + p = json["data"]["P"].as(); + if(json["data"].containsKey("U1")) { + u1 = json["data"]["U1"].as(); + u2 = json["data"]["U2"].as(); + u3 = json["data"]["U3"].as(); + i1 = json["data"]["I1"].as(); + i2 = json["data"]["I2"].as(); + i3 = json["data"]["I3"].as(); + + if(config->hasConfig() && u1 > 0) { + maxPwr = config->fuseSize * u1; + if(u2 > 0) { + maxPwr += config->fuseSize * u2; + if(u3 > 0) { + maxPwr += config->fuseSize * u3; + } + } + } + } else { + if(u1 > 0) { + json["data"]["U1"] = u1; + json["data"]["I1"] = i1; + } + if(u2 > 0) { + json["data"]["U2"] = u2; + json["data"]["I2"] = i2; + } + if(u3 > 0) { + json["data"]["U3"] = u3; + json["data"]["I3"] = i3; + } + } + this->json = json; + } +} + +bool AmsWebServer::checkSecurity(byte level) { + bool access = !config->hasConfig() || config->authSecurity < level; + if(!access && config->authSecurity >= level && server.hasHeader("Authorization")) { + println(" forcing web security"); + String expectedAuth = String(config->authUser) + ":" + String(config->authPass); + + String providedPwd = server.header("Authorization"); + providedPwd.replace("Basic ", ""); + char inputString[providedPwd.length()]; + providedPwd.toCharArray(inputString, providedPwd.length()+1); + + int inputStringLength = sizeof(inputString); + int decodedLength = Base64.decodedLength(inputString, inputStringLength); + char decodedString[decodedLength]; + Base64.decode(decodedString, inputString, inputStringLength); + print("Received auth: "); + println(decodedString); + access = String(decodedString).equals(expectedAuth); + } + + if(!access) { + println(" no access, requesting user/pass"); + server.sendHeader("WWW-Authenticate", "Basic realm=\"Secure Area\""); + server.send(401, "text/html", ""); + } + return access; +} + +void AmsWebServer::indexHtml() { + println("Serving /index.html over http..."); + + if(!checkSecurity(2)) + return; + + String html = String((const __FlashStringHelper*) INDEX_HTML); + html.replace("${version}", VERSION); + html.replace("${data.P}", String(p)); + + html.replace("${data.U1}", u1 > 0 ? String(u1, 1) : ""); + html.replace("${data.I1}", u1 > 0 ? String(i1, 1) : ""); + html.replace("${display.P1}", u1 > 0 ? "" : "none"); + + html.replace("${data.U2}", u2 > 0 ? String(u2, 1) : ""); + html.replace("${data.I2}", u2 > 0 ? String(i2, 1) : ""); + html.replace("${display.P2}", u2 > 0 ? "" : "none"); + + html.replace("${data.U3}", u3 > 0 ? String(u3, 1) : ""); + html.replace("${data.I3}", u3 > 0 ? String(i3, 1) : ""); + html.replace("${display.P3}", u3 > 0 ? "" : "none"); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "text/html", html); +} + +void AmsWebServer::configurationHtml() { + println("Serving /configuration.html over http..."); + + if(!checkSecurity(1)) + return; + + String html = String((const __FlashStringHelper*) CONFIGURATION_HTML); + html.replace("${version}", VERSION); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + + if(config->hasConfig()) { + html.replace("${config.ssid}", config->ssid); + html.replace("${config.ssidPassword}", config->ssidPassword); + html.replace("${config.meterType}", String(config->fuseSize)); + for(int i = 0; i<4; i++) { + html.replace("${config.meterType" + String(i) + "}", config->meterType == i ? "selected" : ""); + } + html.replace("${config.mqtt}", config->mqtt); + html.replace("${config.mqttPort}", String(config->mqttPort)); + html.replace("${config.mqttClientID}", config->mqttClientID); + html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic); + html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic); + html.replace("${config.mqttUser}", config->mqttUser); + html.replace("${config.mqttPass}", config->mqttPass); + html.replace("${config.authUser}", config->authUser); + html.replace("${config.authSecurity}", String(config->authSecurity)); + for(int i = 0; i<3; i++) { + html.replace("${config.authSecurity" + String(i) + "}", config->authSecurity == i ? "selected" : ""); + } + html.replace("${config.authPass}", config->authPass); + html.replace("${config.fuseSize}", String(config->fuseSize)); + for(int i = 0; i<64; i++) { + html.replace("${config.fuseSize" + String(i) + "}", config->fuseSize == i ? "selected" : ""); + } + } else { + html.replace("${config.ssid}", ""); + html.replace("${config.ssidPassword}", ""); + html.replace("${config.meterType}", ""); + for(int i = 0; i<4; i++) { + html.replace("${config.meterType" + String(i) + "}", i == 0 ? "selected" : ""); + } + html.replace("${config.mqtt}", ""); + html.replace("${config.mqttPort}", "1883"); + html.replace("${config.mqttClientID}", ""); + html.replace("${config.mqttPublishTopic}", ""); + html.replace("${config.mqttSubscribeTopic}", ""); + html.replace("${config.mqttUser}", ""); + html.replace("${config.mqttPass}", ""); + html.replace("${config.authSecurity}", ""); + for(int i = 0; i<3; i++) { + html.replace("${config.authSecurity" + String(i) + "}", i == 0 ? "selected" : ""); + } + html.replace("${config.authUser}", ""); + html.replace("${config.authPass}", ""); + html.replace("${config.fuseSize}", ""); + for(int i = 0; i<64; i++) { + html.replace("${config.fuseSize" + String(i) + "}", i == 0 ? "selected" : ""); + } + } + server.send(200, "text/html", html); +} + +void AmsWebServer::bootCss() { + println("Serving /boot.css over http..."); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "text/css", BOOT_CSS); +} + +void AmsWebServer::applicationCss() { + println("Serving /application.css over http..."); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "text/css", APPLICATION_CSS); +} + +void AmsWebServer::gaugemeterJs() { + println("Serving /gaugemeter.js over http..."); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "application/javascript", GAUEGMETER_JS); +} + +void AmsWebServer::indexJs() { + println("Serving /index.js over http..."); + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "application/javascript", INDEX_JS); +} + +void AmsWebServer::dataJson() { + println("Serving /data.json over http..."); + + if(!checkSecurity(2)) + return; + + String jsonStr; + if(!json.isNull()) { + println(" json has data"); + + json["maxPower"] = maxPwr; + json["pct"] = min(p*100/maxPwr, 100); + json["meterType"] = config->meterType; + json["currentMillis"] = millis(); + + serializeJson(json, jsonStr); + } else { + println(" json is empty"); + jsonStr = "{}"; + } + + server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + server.sendHeader("Pragma", "no-cache"); + server.sendHeader("Expires", "-1"); + server.send(200, "application/json", jsonStr); +} + +void AmsWebServer::handleSave() { + String temp; + + temp = server.arg("ssid"); + config->ssid = new char[temp.length() + 1]; + temp.toCharArray(config->ssid, temp.length() + 1, 0); + + temp = server.arg("ssidPassword"); + config->ssidPassword = new char[temp.length() + 1]; + temp.toCharArray(config->ssidPassword, temp.length() + 1, 0); + + config->meterType = (byte)server.arg("meterType").toInt(); + + temp = server.arg("mqtt"); + config->mqtt = new char[temp.length() + 1]; + temp.toCharArray(config->mqtt, temp.length() + 1, 0); + + config->mqttPort = (int)server.arg("mqttPort").toInt(); + + temp = server.arg("mqttClientID"); + config->mqttClientID = new char[temp.length() + 1]; + temp.toCharArray(config->mqttClientID, temp.length() + 1, 0); + + temp = server.arg("mqttPublishTopic"); + config->mqttPublishTopic = new char[temp.length() + 1]; + temp.toCharArray(config->mqttPublishTopic, temp.length() + 1, 0); + + temp = server.arg("mqttSubscribeTopic"); + config->mqttSubscribeTopic = new char[temp.length() + 1]; + temp.toCharArray(config->mqttSubscribeTopic, temp.length() + 1, 0); + + temp = server.arg("mqttUser"); + config->mqttUser = new char[temp.length() + 1]; + temp.toCharArray(config->mqttUser, temp.length() + 1, 0); + + temp = server.arg("mqttPass"); + config->mqttPass = new char[temp.length() + 1]; + temp.toCharArray(config->mqttPass, temp.length() + 1, 0); + + config->authSecurity = (byte)server.arg("authSecurity").toInt(); + + temp = server.arg("authUser"); + config->authUser = new char[temp.length() + 1]; + temp.toCharArray(config->authUser, temp.length() + 1, 0); + + temp = server.arg("authPass"); + config->authPass = new char[temp.length() + 1]; + temp.toCharArray(config->authPass, temp.length() + 1, 0); + + config->fuseSize = (int)server.arg("fuseSize").toInt(); + + println("Saving configuration now..."); + + if (debugger) config->print(debugger); + if (config->save()) + { + println("Successfully saved. Will reboot now."); + String html = "

Successfully Saved!

Device is restarting now...

Go to index"; + server.send(200, "text/html", html); + yield(); + delay(1000); +#if defined(ESP8266) + ESP.reset(); +#elif defined(ESP32) + ESP.restart(); +#endif + } + else + { + println("Error saving configuration"); + String html = "

Error saving configuration!

"; + server.send(500, "text/html", html); + } +} + + +size_t AmsWebServer::print(const char* text) +{ + if (debugger) debugger->print(text); +} +size_t AmsWebServer::println(const char* text) +{ + if (debugger) debugger->println(text); +} +size_t AmsWebServer::print(const Printable& data) +{ + if (debugger) debugger->print(data); +} +size_t AmsWebServer::println(const Printable& data) +{ + if (debugger) debugger->println(data); +} diff --git a/src/AmsWebServer.h b/src/AmsWebServer.h new file mode 100644 index 00000000..921fa3d8 --- /dev/null +++ b/src/AmsWebServer.h @@ -0,0 +1,62 @@ +#ifndef _AMSWEBSERVER_h +#define _AMSWEBSERVER_h + +#include +#include "configuration.h" + +#if defined(ARDUINO) && ARDUINO >= 100 + #include "Arduino.h" +#else + #include "WProgram.h" +#endif + +#if defined(ESP8266) + #include + #include +#elif defined(ESP32) // ARDUINO_ARCH_ESP32 + #include + #include +#else + #warning "Unsupported board type" +#endif + +class AmsWebServer { +public: + void setup(configuration* config, Stream* debugger); + void loop(); + void setJson(StaticJsonDocument<500> json); + +private: + configuration* config; + Stream* debugger; + StaticJsonDocument<500> json; + int maxPwr; + int p; + double u1, u2, u3, i1, i2, i3; + +#if defined(ESP8266) + ESP8266WebServer server; +#elif defined(ESP32) // ARDUINO_ARCH_ESP32 + WebServer server; +#endif + + bool checkSecurity(byte level); + + void indexHtml(); + void configurationHtml(); + void bootCss(); + void applicationCss(); + void gaugemeterJs(); + void indexJs(); + void dataJson(); + + void handleSave(); + + size_t print(const char* text); + size_t println(const char* text); + size_t print(const Printable& data); + size_t println(const Printable& data); + +}; + +#endif diff --git a/src/Base64.cpp b/src/Base64.cpp new file mode 100644 index 00000000..5cf68987 --- /dev/null +++ b/src/Base64.cpp @@ -0,0 +1,142 @@ +/* +Copyright (C) 2016 Arturo Guadalupi. All right reserved. + +This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +*/ + +#include "Base64.h" +#include +#if (defined(__AVR__)) +#include +#else +#include +#endif +const char PROGMEM _Base64AlphabetTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +int Base64Class::encode(char *output, char *input, int inputLength) { + int i = 0, j = 0; + int encodedLength = 0; + unsigned char A3[3]; + unsigned char A4[4]; + + while(inputLength--) { + A3[i++] = *(input++); + if(i == 3) { + fromA3ToA4(A4, A3); + + for(i = 0; i < 4; i++) { + output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[i]]); + } + + i = 0; + } + } + + if(i) { + for(j = i; j < 3; j++) { + A3[j] = '\0'; + } + + fromA3ToA4(A4, A3); + + for(j = 0; j < i + 1; j++) { + output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[j]]); + } + + while((i++ < 3)) { + output[encodedLength++] = '='; + } + } + output[encodedLength] = '\0'; + return encodedLength; +} + +int Base64Class::decode(char * output, char * input, int inputLength) { + int i = 0, j = 0; + int decodedLength = 0; + unsigned char A3[3]; + unsigned char A4[4]; + + + while (inputLength--) { + if(*input == '=') { + break; + } + + A4[i++] = *(input++); + if (i == 4) { + for (i = 0; i <4; i++) { + A4[i] = lookupTable(A4[i]); + } + + fromA4ToA3(A3,A4); + + for (i = 0; i < 3; i++) { + output[decodedLength++] = A3[i]; + } + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) { + A4[j] = '\0'; + } + + for (j = 0; j <4; j++) { + A4[j] = lookupTable(A4[j]); + } + + fromA4ToA3(A3,A4); + + for (j = 0; j < i - 1; j++) { + output[decodedLength++] = A3[j]; + } + } + output[decodedLength] = '\0'; + return decodedLength; +} + +int Base64Class::encodedLength(int plainLength) { + int n = plainLength; + return (n + 2 - ((n + 2) % 3)) / 3 * 4; +} + +int Base64Class::decodedLength(char * input, int inputLength) { + int i = 0; + int numEq = 0; + for(i = inputLength - 1; input[i] == '='; i--) { + numEq++; + } + + return ((6 * inputLength) / 8) - numEq; +} + +//Private utility functions +inline void Base64Class::fromA3ToA4(unsigned char * A4, unsigned char * A3) { + A4[0] = (A3[0] & 0xfc) >> 2; + A4[1] = ((A3[0] & 0x03) << 4) + ((A3[1] & 0xf0) >> 4); + A4[2] = ((A3[1] & 0x0f) << 2) + ((A3[2] & 0xc0) >> 6); + A4[3] = (A3[2] & 0x3f); +} + +inline void Base64Class::fromA4ToA3(unsigned char * A3, unsigned char * A4) { + A3[0] = (A4[0] << 2) + ((A4[1] & 0x30) >> 4); + A3[1] = ((A4[1] & 0xf) << 4) + ((A4[2] & 0x3c) >> 2); + A3[2] = ((A4[2] & 0x3) << 6) + A4[3]; +} + +inline unsigned char Base64Class::lookupTable(char c) { + if(c >='A' && c <='Z') return c - 'A'; + if(c >='a' && c <='z') return c - 71; + if(c >='0' && c <='9') return c + 4; + if(c == '+') return 62; + if(c == '/') return 63; + return -1; +} + +Base64Class Base64; diff --git a/src/Base64.h b/src/Base64.h new file mode 100644 index 00000000..7330225e --- /dev/null +++ b/src/Base64.h @@ -0,0 +1,26 @@ +/* +Copyright (C) 2016 Arturo Guadalupi. All right reserved. + +This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +*/ + +#ifndef _BASE64_H +#define _BASE64_H + +class Base64Class{ + public: + int encode(char *output, char *input, int inputLength); + int decode(char * output, char * input, int inputLength); + int encodedLength(int plainLength); + int decodedLength(char * input, int inputLength); + + private: + inline void fromA3ToA4(unsigned char * A4, unsigned char * A3); + inline void fromA4ToA3(unsigned char * A3, unsigned char * A4); + inline unsigned char lookupTable(char c); +}; +extern Base64Class Base64; + +#endif // _BASE64_H diff --git a/src/application_css.h b/src/application_css.h new file mode 100644 index 00000000..661348ff --- /dev/null +++ b/src/application_css.h @@ -0,0 +1,46 @@ +const char APPLICATION_CSS[] PROGMEM = R"=="==( +.bg-purple { + background-color: var(--purple); +} + + +.GaugeMeter { + position: Relative; + text-align: Center; + overflow: Hidden; + cursor: Default; + display: inline-block; +} + +.GaugeMeter SPAN, .GaugeMeter B { + width: 54%; + position: Absolute; + text-align: Center; + display: Inline-Block; + color: RGBa(0,0,0,.8); + font-weight: 100; + font-family: "Open Sans", Arial; + overflow: Hidden; + white-space: NoWrap; + text-overflow: Ellipsis; + margin: 0 23%; +} + +.GaugeMeter[data-style="Semi"] B { + width: 80%; + margin: 0 10%; +} + +.GaugeMeter S, .GaugeMeter U { + text-decoration: None; + font-size: .60em; + font-weight: 200; + opacity: .6; +} + +.GaugeMeter B { + color: #000; + font-weight: 200; + opacity: .8; +} +)=="=="; diff --git a/src/boot_css.h b/src/boot_css.h new file mode 100644 index 00000000..342066a8 --- /dev/null +++ b/src/boot_css.h @@ -0,0 +1,327 @@ +const char BOOT_CSS[] PROGMEM = R"=="==( +/* Ripped necessary style from bootstrap 4.4.1 to make the page look good without internet access. Meant to be overridden by CSS from CDN */ +:root { + --blue: #007bff; + --indigo: #6610f2; + --purple: #6f42c1; + --pink: #e83e8c; + --red: #dc3545; + --orange: #fd7e14; + --yellow: #ffc107; + --green: #28a745; + --teal: #20c997; + --cyan: #17a2b8; + --white: #fff; + --gray: #6c757d; + --gray-dark: #343a40; + --primary: #007bff; + --secondary: #6c757d; + --success: #28a745; + --info: #17a2b8; + --warning: #ffc107; + --danger: #dc3545; + --light: #f8f9fa; + --dark: #343a40; + --breakpoint-xs: 0; + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --font-family-sans-serif: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + --font-family-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace; +} +html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; + color: -internal-root-color; +} +body { + display: block; + margin: 8px; + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; +} +.bg-white { + background-color: #fff!important; +} +.bg-light { + background-color: #f8f9fa!important; +} +.bg-purple { + background-color: var(--purple); +} +.text-white-50 { + color: rgba(255,255,255,.5)!important; +} +.mb-0, .my-0 { + margin-bottom: 0!important; +} +.mb-2, .my-2 { + margin-bottom: .5rem!important; +} +.mt-2, .my-2 { + margin-top: .5rem!important; +} +.pb-2, .py-2 { + padding-bottom: .5rem!important; +} +.p-3 { + padding: 1rem!important; +} +.mb-3, .my-3 { + margin-bottom: 1rem!important; +} +.mt-3, .my-3 { + margin-top: 1rem!important; +} +.mb-4, .my-4 { + margin-bottom: 1.5rem!important; +} +.shadow { + box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; +} +.align-items-center { + -ms-flex-align: center!important; + align-items: center!important; +} +.border-bottom { + border-bottom: 1px solid #dee2e6!important; +}.rounded { + border-radius: .25rem!important; +} +div { + display: block; +} +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { + display: block; +} +main { + display: block; +} +.text-white { + color: #fff!important; +} +.text-right { + text-align: right!important; +} +.text-center { + text-align: center!important; +} +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + margin-bottom: .5rem; + font-weight: 500; + line-height: 1.2; +} +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: .5rem; +} +.h6, h6 { + font-size: 1rem; +} +.row { + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} +.d-flex { + display: -ms-flexbox!important; + display: flex!important; +} +.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} +.col-2 { + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; +} +.col-3 { + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; +} +.col-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; +} +.col-5 { + -ms-flex: 0 0 41.666667%; + flex: 0 0 41.666667%; + max-width: 41.666667%; +} +.col-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; +} +.col-8 { + -ms-flex: 0 0 66.666667%; + flex: 0 0 66.666667%; + max-width: 66.666667%; +} +.col-9 { + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; +} +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} +.btn { + display: inline-block; + font-weight: 400; + color: #212529; + text-align: center; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: .375rem .75rem; + font-size: 1rem; + line-height: 1.5; + border-radius: .25rem; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; +} +.btn-outline-secondary { + color: #6c757d; + border-color: #6c757d; +} +.btn-primary { + color: #fff; + background-color: #007bff; + border-color: #007bff; +} +.form-group { + margin-bottom: 1rem; +} +.form-control { + display: block; + width: 100%; + height: calc(1.5em + .75rem + 2px); + padding: .375rem .75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #495057; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #ced4da; + border-radius: .25rem; + transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; +} +input:not([type="image" i]) { + box-sizing: border-box; +} +input { + -webkit-writing-mode: horizontal-tb !important; + text-rendering: auto; + color: -internal-light-dark-color(black, white); + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0px; + text-shadow: none; + display: inline-block; + text-align: start; + -webkit-appearance: textfield; + background-color: -internal-light-dark-color(white, black); + -webkit-rtl-ordering: logical; + cursor: text; + margin: 0em; + font: 400 13.3333px Arial; + padding: 1px 0px; + border-width: 2px; + border-style: inset; + border-color: initial; + border-image: initial; +} +button, input { + overflow: visible; +} +button, input, optgroup, select, textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0,0,0,.1); + box-sizing: content-box; + height: 0; + overflow: visible; +} + +@media (min-width: 576px) { + .container, .container-sm { + max-width: 540px; + } +} +@media (min-width: 768px) { + .container, .container-md, .container-sm { + max-width: 720px; + } + .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + .col-md-6 { + -ms-flex: 0 0 50%; + flex: 0 0 50%; + max-width: 50%; + } +} +@media (min-width: 992px) { + .container, .container-lg, .container-md, .container-sm { + max-width: 960px; + } + .col-lg-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } +} +@media (min-width: 1200px) { + .container, .container-lg, .container-md, .container-sm, .container-xl { + max-width: 1140px; + } +} + +*, ::after, ::before { + box-sizing: border-box; +} +*, ::after, ::before { + box-sizing: border-box; +} +)=="=="; diff --git a/src/configuration_html.h b/src/configuration_html.h new file mode 100644 index 00000000..28e8028c --- /dev/null +++ b/src/configuration_html.h @@ -0,0 +1,148 @@ +const char CONFIGURATION_HTML[] PROGMEM = R"=="==( + + + + + AMS reader - configuration + + + + + + +
+
+
+
AMS reader - configuration
+ ${version} +
+
+
+
+
+
+
WiFi
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
AMS meter
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
MQTT
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
Web server
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+
+
+ Back +
+
+ +
+
+
+
+ + +)=="=="; diff --git a/src/gaugemeter_js.h b/src/gaugemeter_js.h new file mode 100644 index 00000000..5eeb432a --- /dev/null +++ b/src/gaugemeter_js.h @@ -0,0 +1,277 @@ +const char GAUEGMETER_JS[] PROGMEM = R"=="==( +/* + * AshAlom Gauge Meter. Version 2.0.0 + * Copyright AshAlom.com All rights reserved. + * https://github.com/AshAlom/GaugeMeter <- Deleted! + * https://github.com/githubsrinath/GaugeMeter <- Backup original. + * + * Original created by Dr Ash Alom + * + * This is a bug fixed and modified version of the AshAlom Gauge Meter. + * Copyright 2018 Michael Wolf (Mictronics) + * https://github.com/mictronics/GaugeMeter + * + */ +!function ($) { + $.fn.gaugeMeter = function (t) { + var defaults = $.extend({ + id: "", + percent: 0, + used: null, + min: null, + total: null, + size: 100, + prepend: "", + append: "", + theme: "Red-Gold-Green", + color: "", + back: "RGBa(0,0,0,.06)", + width: 3, + style: "Full", + stripe: "0", + animationstep: 1, + animate_gauge_colors: false, + animate_text_colors: false, + label: "", + label_color: "Black", + text: "", + text_size: 0.22, + fill: "", + showvalue: false + }, t); + return this.each(function () { + + function getThemeColor(e) { + var t = "#2C94E0"; + return e || (e = 1e-14), + "Red-Gold-Green" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#e32100"), e > 20 && (t = "#f35100"), e > 30 && (t = "#ff8700"), e > 40 && (t = "#ffb800"), e > 50 && (t = "#ffd900"), e > 60 && (t = "#dcd800"), e > 70 && (t = "#a6d900"), e > 80 && (t = "#69d900"), e > 90 && (t = "#32d900")), + "Green-Gold-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#69d900"), e > 20 && (t = "#a6d900"), e > 30 && (t = "#dcd800"), e > 40 && (t = "#ffd900"), e > 50 && (t = "#ffb800"), e > 60 && (t = "#ff8700"), e > 70 && (t = "#f35100"), e > 80 && (t = "#e32100"), e > 90 && (t = "#d90000")), + "Green-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#41c900"), e > 20 && (t = "#56b300"), e > 30 && (t = "#6f9900"), e > 40 && (t = "#8a7b00"), e > 50 && (t = "#a75e00"), e > 60 && (t = "#c24000"), e > 70 && (t = "#db2600"), e > 80 && (t = "#f01000"), e > 90 && (t = "#ff0000")), + "Red-Green" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#f01000"), e > 20 && (t = "#db2600"), e > 30 && (t = "#c24000"), e > 40 && (t = "#a75e00"), e > 50 && (t = "#8a7b00"), e > 60 && (t = "#6f9900"), e > 70 && (t = "#56b300"), e > 80 && (t = "#41c900"), e > 90 && (t = "#32d900")), + "DarkBlue-LightBlue" === option.theme && (e > 0 && (t = "#2c94e0"), e > 10 && (t = "#2b96e1"), e > 20 && (t = "#2b99e4"), e > 30 && (t = "#2a9ce7"), e > 40 && (t = "#28a0e9"), e > 50 && (t = "#26a4ed"), e > 60 && (t = "#25a8f0"), e > 70 && (t = "#24acf3"), e > 80 && (t = "#23aff5"), e > 90 && (t = "#21b2f7")), + "LightBlue-DarkBlue" === option.theme && (e > 0 && (t = "#21b2f7"), e > 10 && (t = "#23aff5"), e > 20 && (t = "#24acf3"), e > 30 && (t = "#25a8f0"), e > 40 && (t = "#26a4ed"), e > 50 && (t = "#28a0e9"), e > 60 && (t = "#2a9ce7"), e > 70 && (t = "#2b99e4"), e > 80 && (t = "#2b96e1"), e > 90 && (t = "#2c94e0")), + "DarkRed-LightRed" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#dc0000"), e > 20 && (t = "#e00000"), e > 30 && (t = "#e40000"), e > 40 && (t = "#ea0000"), e > 50 && (t = "#ee0000"), e > 60 && (t = "#f30000"), e > 70 && (t = "#f90000"), e > 80 && (t = "#fc0000"), e > 90 && (t = "#ff0000")), + "LightRed-DarkRed" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#fc0000"), e > 20 && (t = "#f90000"), e > 30 && (t = "#f30000"), e > 40 && (t = "#ee0000"), e > 50 && (t = "#ea0000"), e > 60 && (t = "#e40000"), e > 70 && (t = "#e00000"), e > 80 && (t = "#dc0000"), e > 90 && (t = "#d90000")), + "DarkGreen-LightGreen" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#33db00"), e > 20 && (t = "#34df00"), e > 30 && (t = "#34e200"), e > 40 && (t = "#36e700"), e > 50 && (t = "#37ec00"), e > 60 && (t = "#38f100"), e > 70 && (t = "#38f600"), e > 80 && (t = "#39f900"), e > 90 && (t = "#3afc00")), + "LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")), + "DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")), + "LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")), + "White" === option.theme && (t = "#fff"), + "Black" === option.theme && (t = "#000"), + t; + } + /* The label below gauge. */ + function createLabel(t, a) { + if(t.children("b").length === 0){ + $("").appendTo(t).html(option.label).css({ + "line-height": option.size + 5 * a + "px", + color: option.label_color + }); + } + } + /* Prepend and append text, the gauge text or percentage value. */ + function createSpanTag(t) { + var fgcolor = ""; + if (option.animate_text_colors === true){ + fgcolor = option.fgcolor; + } + var child = t.children("span"); + if(child.length !== 0){ + child.html(r).css({color: fgcolor}); + return; + } + if(option.text_size <= 0.0 || Number.isNaN(option.text_size)){ + option.text_size = 0.22; + } + if(option.text_size > 0.5){ + option.text_size = 0.5; + } + $("").appendTo(t).html(r).css({ + "line-height": option.size + "px", + "font-size": option.text_size * option.size + "px", + color: fgcolor + }); + } + /* Get data attributes as options from div tag. Fall back to defaults when not exists. */ + function getDataAttr(t) { + $.each(dataAttr, function (index, element) { + if(t.data(element) !== undefined && t.data(element) !== null){ + option[element] = t.data(element); + } else { + option[element] = $(defaults).attr(element); + } + + if(element === "fill"){ + s = option[element]; + } + + if((element === "size" || + element === "width" || + element === "animationstep" || + element === "stripe" + ) && !Number.isInteger(option[element])){ + option[element] = parseInt(option[element]); + } + + if(element === "text_size"){ + option[element] = parseFloat(option[element]); + } + }); + } + /* Draws the gauge. */ + function drawGauge(a) { + if(M < 0) M = 0; + if(M > 100) M = 100; + var lw = option.width < 1 || isNaN(option.width) ? option.size / 20 : option.width; + g.clearRect(0, 0, b.width, b.height); + g.beginPath(); + g.arc(m, v, x, G, k, !1); + if(s){ + g.fillStyle = option.fill; + g.fill(); + } + g.lineWidth = lw; + g.strokeStyle = option.back; + option.stripe > parseInt(0) ? g.setLineDash([option.stripe], 1) : g.lineCap = "round"; + g.stroke(); + g.beginPath(); + g.arc(m, v, x, -I, P * a - I, !1); + g.lineWidth = lw; + g.strokeStyle = option.fgcolor; + g.stroke(); + c > M && (M += z, requestAnimationFrame(function(){ + drawGauge(Math.min(M, c) / 100); + }, p)); + } + + $(this).attr("data-id", $(this).attr("id")); + var r, + dataAttr = ["percent", + "used", + "min", + "total", + "size", + "prepend", + "append", + "theme", + "color", + "back", + "width", + "style", + "stripe", + "animationstep", + "animate_gauge_colors", + "animate_text_colors", + "label", + "label_color", + "text", + "text_size", + "fill", + "showvalue"], + option = {}, + c = 0, + p = $(this), + s = false; + p.addClass("gaugeMeter"); + getDataAttr(p); + + if(Number.isInteger(option.used) && Number.isInteger(option.total)){ + var u = option.used; + var t = option.total; + if(Number.isInteger(option.min)) { + if(option.min < 0) { + t -= option.min; + u -= option.min; + } + } + c = u / (t / 100); + } else { + if(Number.isInteger(option.percent)){ + c = option.percent; + } else { + c = parseInt(defaults.percent); + } + } + if(c < 0) c = 0; + if(c > 100) c = 100; + + if( option.text !== "" && option.text !== null && option.text !== undefined){ + if(option.append !== "" && option.append !== null && option.append !== undefined){ + r = option.text + "" + option.append + ""; + } else { + r = option.text; + } + if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){ + r = "" + option.prepend + "" + r; + } + } else { + if(defaults.showvalue === true || option.showvalue === true){ + r = option.used; + } else { + r = c.toString(); + } + if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){ + r = "" + option.prepend + "" + r; + } + + if(option.append !== "" && option.append !== null && option.append !== undefined){ + r = r + "" + option.append + ""; + } + } + + option.fgcolor = getThemeColor(c); + if(option.color !== "" && option.color !== null && option.color !== undefined){ + option.fgcolor = option.color; + } + + if(option.animate_gauge_colors === true){ + option.fgcolor = getThemeColor(c); + } + createSpanTag(p); + + if(option.style !== "" && option.style !== null && option.style !== undefined){ + createLabel(p, option.size / 13); + } + + $(this).width(option.size + "px"); + + var b = $("").attr({width: option.size, height: option.size}).get(0), + g = b.getContext("2d"), + m = b.width / 2, + v = b.height / 2, + _ = 360 * option.percent, + x = (_ * (Math.PI / 180), b.width / 2.5), + k = 2.3 * Math.PI, + G = 0, + M = 0 === option.animationstep ? c : 0, + z = Math.max(option.animationstep, 0), + P = 2 * Math.PI, + I = Math.PI / 2, + R = option.style; + var child = $(this).children("canvas"); + if(child.length !== 0){ + /* Replace existing canvas when new percentage was written. */ + child.replaceWith(b); + } else { + /* Initially create canvas. */ + $(b).appendTo($(this)); + } + + if ("Semi" === R){ + k = 2 * Math.PI; + G = 3.13; + P = 1 * Math.PI; + I = Math.PI / .996; + } + if ("Arch" === R){ + k = 2.195 * Math.PI; + G = 1, G = 655.99999; + P = 1.4 * Math.PI; + I = Math.PI / .8335; + } + drawGauge(M / 100); + }); + }; +} +(jQuery); +)=="=="; diff --git a/src/index_html.h b/src/index_html.h new file mode 100644 index 00000000..3fbeb677 --- /dev/null +++ b/src/index_html.h @@ -0,0 +1,79 @@ +const char INDEX_HTML[] PROGMEM = R"=="==( + + + + + AMS reader + + + + + + + + +
+
+
+
AMS reader
+ ${version} +
+
+
+
Current meter values
+
+
+
+
+ ${data.P} W +
+ +
+
+
+
+
P1
+
${data.U1} V
+
${data.I1} A
+
+
+
P2
+
${data.U2} V
+
${data.I2} A
+
+
+
P3
+
${data.U3} V
+
${data.I3} A
+
+
+
+
+
+ +
+ + + +)=="=="; diff --git a/src/index_js.h b/src/index_js.h new file mode 100644 index 00000000..5a02bfe4 --- /dev/null +++ b/src/index_js.h @@ -0,0 +1,80 @@ +const char INDEX_JS[] PROGMEM = R"=="==( +$(".GaugeMeter").gaugeMeter(); + +var wait = 500; +var nextrefresh = wait; +var fetch = function() { + $.ajax({ + url: '/data.json', + dataType: 'json', + }).done(function(json) { + $(".SimpleMeter").hide(); + var el = $(".GaugeMeter"); + el.show(); + var rate = 2500; + if(json.data) { + el.data('percent', json.pct); + if(json.data.P) { + var num = parseFloat(json.data.P); + if(num > 1000) { + num = num / 1000; + el.data('text', num.toFixed(1)); + el.data('append','kW'); + } else { + el.data('text', num); + el.data('append','W'); + } + } + el.gaugeMeter(); + + for(var id in json.data) { + var str = json.data[id]; + if(isNaN(str)) { + $('#'+id).html(str); + } else { + var num = parseFloat(str); + $('#'+id).html(num.toFixed(1)); + } + } + + if(json.data.U1 > 0) { + $('#P1').show(); + } + + if(json.data.U2 > 0) { + $('#P2').show(); + } + + if(json.data.U3 > 0) { + $('#P3').show(); + } + + if(json.meterType == 3) { + rate = 10000; + } + if(json.currentMillis && json.up) { + nextrefresh = rate - ((json.currentMillis - json.up) % rate) + wait; + } else { + nextrefresh = 2500; + } + } else { + el.data('percent', 0); + el.data('text', '-'); + el.gaugeMeter(); + nextrefresh = 2500; + } + if(!nextrefresh || nextrefresh < 500) { + nextrefresh = 2500; + } + setTimeout(fetch, nextrefresh); + }).fail(function() { + el.data('percent', 0); + el.data('text', '-'); + el.gaugeMeter(); + nextrefresh = 10000; + setTimeout(fetch, nextrefresh); + }); +} +setTimeout(fetch, nextrefresh); + +)=="==";