#include #include #include #include #include #include //https://github.com/PaulStoffregen/Time #include #include #include #include extern const unsigned char caCert[] PROGMEM; extern const unsigned int caCertLen; //IMPORTANT: Specify your WIFI settings: //#define WIFI_SSID "NOS-3B26" // setubal //#define WIFI_PASS "RMKSX2GL" // Setubal #define WIFI_SSID "MEO-AA9030"// Andre #define WIFI_PASS "81070ce635" // andre //IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below): //#define ENABLE_MQTT #ifdef ENABLE_MQTT //NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt. //NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc" //#define MQTT_SERVER "cc42fcb4f1eb492fa3c2e07a9e617830.s2.eu.hivemq.cloud" //"192.168.1.123" #define MQTT_SERVER "192.168.1.123" #define MQTT_PORT 1883 // 8883 #define MQTT_USER "tezmqtt" #define MQTT_PASSWORD "mqtTez_123" #define MQTT_TOPIC_ROOT "home/grid_battery/" //this is where mqtt data will be pushed #define MQTT_PUSH_FREQ_SEC 2 //maximum mqtt update frequency in seconds #endif ENABLE_MQTT WiFiClient espClient; //WiFiClientSecure espClient; PubSubClient mqttClient(espClient); IPAddress thisip; ESP8266WebServer server(80); SimpleTimer timer; char g_szRecvBuff[7000]; circular_log<7000> g_log; bool ntpTimeReceived = false; int g_baudRate = 0; void Log(const char* msg) { g_log.Log(msg); } int LEDPIN = 2; // The on-board Wemos D1 mini LED ////////////////////////////////////// void setup() { Serial.begin(115200); Serial.println("---"); Serial.println("Serial started"); pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); // LOW = on delay(2000); digitalWrite(LED_BUILTIN, HIGH); // HIGH = off // connect to WiFi WiFi.mode(WIFI_STA); WiFi.persistent(false); //our credentials are hardcoded, so we don't need ESP saving those each boot (will save on flash wear) WiFi.hostname("PylonBattery"); Serial.println(); Serial.print("connecting to "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); Serial.println(""); for(int i=1; i<=3; i++){ LedBlink(); } ArduinoOTA.setHostname("AndrePylon"); ArduinoOTA.begin(); server.on("/", handleRoot); server.on("/log", handleLog); server.on("/req", handleReq); server.on("/jsonOut", handleJsonOut); server.on("/reboot", [](){ ESP.restart(); }); server.begin(); Serial.println("web server started"); Serial.println(""); delay(1000); syncTime(); Serial.println("SNTP time synced"); Serial.println(""); for(int i=1; i<=3; i++){ LedBlink(); } /* COMMENTED BY TEZ */ /* // https://links2004.github.io/Arduino/d2/d2f/class_wi_fi_client_secure.html // Load root certificate in DER format into WiFiClientSecure object // bool res = espClient.setCACert_P(caCert, caCertLen); bool res = espClient.setCertificate(caCert, caCertLen); if (!res) { Serial.println("Failed to load root CA certificate!"); while (true) { yield(); } } Serial.println("local load root CA certificate OK!"); // VERIFY SSL FINGERPRINT if (!espClient.verify( "49 7E 82 A3 DB B4 19 1E 73 E5 19 A6 D3 C6 C5 31 DE D9 DE 97 BA B1 D8 19 80 A3 88 96 70 8D 94 3D", "websocketclient.hivemq.cloud") ) { Serial.println( "Fingerprint certificate NOT verified sorry!" ); // mqttClient.disconnect(); // return false; }else{ Serial.println( "Fingerprint verified OK!"); } */ // END COMMENTED BY TEZ #ifdef ENABLE_MQTT mqttClient.setServer(MQTT_SERVER, MQTT_PORT); #endif Log("Boot event"); } ////////////////////////////////////////////////// void LedBlink(){ digitalWrite(LED_BUILTIN, LOW); delay(150); digitalWrite(LED_BUILTIN, HIGH); // high = off delay(150); } ////////////////////////////////////////////////// void handleLog() { server.send(200, "text/html", g_log.c_str()); } ////////////////////////////////////////////////// void switchBaud(int newRate) { if(g_baudRate == newRate) { return; } if(g_baudRate != 0) { Serial.flush(); delay(20); Serial.end(); delay(20); } char szMsg[50]; snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate); Log(szMsg); Serial.begin(newRate); g_baudRate = newRate; delay(20); } ////////////////////////////////////////////////// void waitForSerial() { for(int ix=0; ix<150;ix++) { if(Serial.available()) break; delay(10); } } ////////////////////////////////////////////////// int readFromSerial() { memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); int recvBuffLen = 0; bool foundTerminator = true; waitForSerial(); while(Serial.available()) { char szResponse[256] = ""; const int readNow = Serial.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end) if(readNow > 0 && szResponse[0] != '\0') { if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff))) { Log("WARNING: Read too much data on the console!"); break; } strcat(g_szRecvBuff, szResponse); recvBuffLen += readNow; if(strstr(g_szRecvBuff, "$$\r\n\rpylon")) { strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add foundTerminator = true; break; //found end of the string } if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit")) { //we need to send new line character so battery continues the output Serial.write("\r"); } waitForSerial(); } } if(recvBuffLen > 0 ) { if(foundTerminator == false) { Log("Failed to find pylon> terminator"); } } return recvBuffLen; } ////////////////////////////////////////////////// bool readFromSerialAndSendResponse() { const int recvBuffLen = readFromSerial(); if(recvBuffLen > 0) { server.sendContent(g_szRecvBuff); return true; } return false; } ////////////////////////////////////////////////// bool sendCommandAndReadSerialResponse(const char* pszCommand) { switchBaud(115200); if(pszCommand[0] != '\0') { Serial.write(pszCommand); } Serial.write("\n"); const int recvBuffLen = readFromSerial(); if(recvBuffLen > 0) { return true; } //wake up console and try again: wakeUpConsole(); if(pszCommand[0] != '\0') { Serial.write(pszCommand); } Serial.write("\n"); return readFromSerial() > 0; } ////////////////////////////////////////////////// void handleReq() { bool respOK; if(server.hasArg("code") == false) { respOK = sendCommandAndReadSerialResponse(""); } else { respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str()); } if(respOK) { server.send(200, "text/plain", g_szRecvBuff); } else { server.send(500, "text/plain", "????"); } } ////////////////////////////////////////////////// void handleJsonOut() { if(sendCommandAndReadSerialResponse("pwr") == false) { server.send(500, "text/plain", "Failed to get response to 'pwr' command"); return; } parsePwrResponse(g_szRecvBuff); prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff)); server.send(200, "application/json", g_szRecvBuff); } ////////////////////////////////////////////////// void handleRoot() { unsigned long days = 0, hours = 0, minutes = 0; unsigned long val = os_getCurrentTimeSec(); days = val / (3600*24); val -= days * (3600*24); hours = val / 3600; val -= hours * 3600; minutes = val / 60; val -= minutes*60; static char szTmp[2500] = ""; snprintf(szTmp, sizeof(szTmp)-1, "Garage Battery
Time GMT: %d/%02d/%02d %02d:%02d:%02d (%s)
Uptime: %02d:%02d:%02d.%02d

free heap: %u
Wifi RSSI: %d
Wifi SSID: %s", year(), month(), day(), hour(), minute(), second(), "GMT", (int)days, (int)hours, (int)minutes, (int)val, ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str()); strncat(szTmp, "
Runtime log
", sizeof(szTmp)-1); strncat(szTmp, "
Command:
Power | Help | Event Log | Time", sizeof(szTmp)-1); strncat(szTmp, "", sizeof(szTmp)-1); server.send(200, "text/html", szTmp); } unsigned long os_getCurrentTimeSec() { static unsigned int wrapCnt = 0; static unsigned long lastVal = 0; unsigned long currentVal = millis(); if(currentVal < lastVal) { wrapCnt++; } lastVal = currentVal; unsigned long seconds = currentVal/1000; //millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter return (wrapCnt*4294967) + seconds; } ////////////////////////////////////////////////// void syncTime() { // configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); //get time from NTP time_t currentTimeGMT = getNtpTime(); if(currentTimeGMT) { ntpTimeReceived = true; setTime(currentTimeGMT); } else { timer.setTimeout(3000, syncTime); //try again in 5 seconds } struct tm timeinfo; gmtime_r(¤tTimeGMT, &timeinfo); Serial.print("Current time: "); Serial.print(asctime(&timeinfo)); Serial.println("------------------"); // Synchronize time using SNTP. This is necessary to verify that // the TLS certificates offered by the server are currently valid. // Serial.print("Setting time using SNTP"); // configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); // time_t now = time(nullptr); // while (now < 8 * 3600 * 2) { // delay(500); // Serial.print("."); // now = time(nullptr); // } // // setTime(now); // // Serial.println(""); // struct tm timeinfo; // gmtime_r(&now, &timeinfo); // Serial.print("Current time: "); // Serial.print(asctime(&timeinfo)); } ////////////////////////////////////////////////// void wakeUpConsole() { switchBaud(1200); //byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D}; //Serial.write(wakeUpBuff, sizeof(wakeUpBuff)); Serial.write("~20014682C0048520FCC3\r"); delay(1000); byte newLineBuff[] = {0x0E, 0x0A}; switchBaud(115200); for(int ix=0; ix<10; ix++) { Serial.write(newLineBuff, sizeof(newLineBuff)); delay(1000); if(Serial.available()) { while(Serial.available()) { Serial.read(); } break; } } } #define MAX_PYLON_BATTERIES 2 struct pylonBattery { bool isPresent; long soc; //Coulomb in % long voltage; //in mW long current; //in mA, negative value is discharge long tempr; //temp of case or BMS? long cellTempLow; long cellTempHigh; long cellVoltLow; long cellVoltHigh; char baseState[9]; //Charge | Dischg | Idle char voltageState[9]; //Normal char currentState[9]; //Normal char tempState[9]; //Normal char time[20]; //2019-06-08 04:00:29 char b_v_st[9]; //Normal (battery voltage?) char b_t_st[9]; //Normal (battery temperature?) bool isCharging() const { return strcmp(baseState, "Charge") == 0; } bool isDischarging() const { return strcmp(baseState, "Dischg") == 0; } bool isIdle() const { return strcmp(baseState, "Idle") == 0; } bool isBalancing() const { return strcmp(baseState, "Balance") == 0; } bool isNormal() const { if(isCharging() == false && isDischarging() == false && isIdle() == false && isBalancing() == false) { return false; //base state looks wrong! } return strcmp(voltageState, "Normal") == 0 && strcmp(currentState, "Normal") == 0 && strcmp(tempState, "Normal") == 0 && strcmp(b_v_st, "Normal") == 0 && strcmp(b_t_st, "Normal") == 0 ; } }; struct batteryStack { int batteryCount; int soc; //in %, if charging: average SOC, otherwise: lowest SOC int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest long currentDC; //mAh current going in or out of the battery long avgVoltage; //in mV char baseState[9]; //Charge | Dischg | Idle | Balance | Alarm! pylonBattery batts[MAX_PYLON_BATTERIES]; bool isNormal() const { for(int ix=0; ix 1000) { return (long)(powerDC*1.06); } else if(powerDC > 600) { return (long)(powerDC*1.1); } else { return (long)(powerDC*1.13); } } } }; batteryStack g_stack; ////////////////////////////////////////////////// long extractInt(const char* pStr, int pos) { return atol(pStr+pos); } ////////////////////////////////////////////////// void extractStr(const char* pStr, int pos, char* strOut, int strOutSize) { strOut[strOutSize-1] = '\0'; strncpy(strOut, pStr+pos, strOutSize-1); strOutSize--; //trim right while(strOutSize > 0) { if(isspace(strOut[strOutSize-1])) { strOut[strOutSize-1] = '\0'; } else { break; } strOutSize--; } } /* Output has mixed \r and \r\n pwr @ Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St 1 49735 -1440 22000 19000 19000 3315 3317 Dischg Normal Normal Normal 93% 2019-06-08 04:00:30 Normal Normal .... 8 - - - - - - - Absent - - - - - - - Command completed successfully $$ pylon */ ////////////////////////////////////////////////// bool parsePwrResponse(const char* pStr) { if(strstr(pStr, "Command completed successfully") == NULL) { return false; } int chargeCnt = 0; int dischargeCnt = 0; int idleCnt = 0; int alarmCnt = 0; int socAvg = 0; int socLow = 0; int tempHigh = 0; int tempLow = 0; memset(&g_stack, 0, sizeof(g_stack)); for(int ix=0; ix g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;} if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;} if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;} } } } //now update stack state: g_stack.avgVoltage /= g_stack.batteryCount; g_stack.soc = socLow; if(tempHigh > 15000) //15C { g_stack.temp = tempHigh; //in the summer we highlight the warmest cell } else { g_stack.temp = tempLow; //in the winter we focus on coldest cell } if(alarmCnt > 0) { strcpy(g_stack.baseState, "Alarm!"); } else if(chargeCnt == g_stack.batteryCount) { strcpy(g_stack.baseState, "Charge"); g_stack.soc = (int)(socAvg / g_stack.batteryCount); } else if(dischargeCnt == g_stack.batteryCount) { strcpy(g_stack.baseState, "Dischg"); } else if(idleCnt == g_stack.batteryCount) { strcpy(g_stack.baseState, "Idle"); } else { strcpy(g_stack.baseState, "Balance"); } return true; } ////////////////////////////////////////////////// void prepareJsonOutput(char* pBuff, int buffSize) { memset(pBuff, 0, buffSize); snprintf(pBuff, buffSize-1, "{\"soc\": %d, \"temp\": %d, \"currentDC\": %ld, \"avgVoltage\": %ld, \"baseState\": \"%s\", \"batteryCount\": %d, \"powerDC\": %ld, \"estPowerAC\": %ld, \"isNormal\": %s}", g_stack.soc, g_stack.temp, g_stack.isNormal() ? "true" : "false"); } ////////////////////////////////////////////////// void loop() { #ifdef ENABLE_MQTT mqttLoop(); #endif ArduinoOTA.handle(); server.handleClient(); timer.run(); //if there are bytes availbe on serial here - it's unexpected //when we send a command to battery, we read whole response //if we get anything here anyways - we will log it int bytesAv = Serial.available(); if(bytesAv > 0) { if(bytesAv > 63) { bytesAv = 63; } char buff[64+4] = "RCV:"; if(Serial.readBytes(buff+4, bytesAv) > 0) { digitalWrite(LED_BUILTIN, LOW); delay(5); digitalWrite(LED_BUILTIN, HIGH);//high is off Log(buff); } } } ////////////////////////////////////////////////// #ifdef ENABLE_MQTT #define ABS_DIFF(a, b) (a > b ? a-b : b-a) ////////////////////////////////////////////////// void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force) { char szTmp[16] = ""; snprintf(szTmp, 15, "%.2f", newValue); if(force || ABS_DIFF(newValue, oldValue) > minDiff) { mqttClient.publish(topic, szTmp, false); } } ////////////////////////////////////////////////// void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force) { char szTmp[16] = ""; snprintf(szTmp, 15, "%d", newValue); if(force || ABS_DIFF(newValue, oldValue) > minDiff) { mqttClient.publish(topic, szTmp, false); } } ////////////////////////////////////////////////// void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force) { if(force || strcmp(newValue, oldValue) != 0) { mqttClient.publish(topic, newValue, false); } } ////////////////////////////////////////////////// void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */) { Serial.println("entered publish"); mqtt_publish_f(MQTT_TOPIC_ROOT "soc", g_stack.soc, lastSentData.soc, 0, forceUpdate); mqtt_publish_f(MQTT_TOPIC_ROOT "temp", (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0, forceUpdate); mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC", g_stack.getEstPowerAc(), lastSentData.getEstPowerAc(), 10, forceUpdate); mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount, lastSentData.batteryCount, 0, forceUpdate); mqtt_publish_s(MQTT_TOPIC_ROOT "base_state", g_stack.baseState, lastSentData.baseState , forceUpdate); mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal", g_stack.isNormal() ? 1:0, lastSentData.isNormal() ? 1:0, 0, forceUpdate); Serial.println("finished publish"); } ////////////////////////////////////////////////// void mqttLoop() { //if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that) static unsigned long g_lastConnectionAttempt = 0; //first: let's make sure we are connected to mqtt const char* topicLastWill = MQTT_TOPIC_ROOT "availability"; if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) { if(mqttClient.connect("GarageBattery", MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline")) { Log("Connected to MQTT server: " MQTT_SERVER); Serial.println("Connected to MQTT server!!!"); mqttClient.publish(topicLastWill, "online", true); // test by TeZ mqttClient.publish("home/grid_battery/tez", "cicciobello", true); mqttClient.publish("testbytez", "232323", true); } else { Log("Failed to connect to MQTT server."); Serial.println("Failed to connect to MQTT server."); } g_lastConnectionAttempt = os_getCurrentTimeSec(); } //next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds) static unsigned long g_lastDataSent = 0; if(mqttClient.connected() && os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC && sendCommandAndReadSerialResponse("pwr") == true) { Serial.println("Connected and sending to MQTT server!!!"); static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again static unsigned int callCnt = 0; parsePwrResponse(g_szRecvBuff); bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call pushBatteryDataToMqtt(lastSentData, forceUpdate); callCnt++; g_lastDataSent = os_getCurrentTimeSec(); memcpy(&lastSentData, &g_stack, sizeof(batteryStack)); } mqttClient.loop(); } #endif //ENABLE_MQTT