experiments with Pylontech/GroWatt PV tech
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

898 lines
24 KiB

  1. #include <ESP8266WiFi.h>
  2. #include <ESP8266mDNS.h>
  3. #include <ArduinoOTA.h>
  4. #include <ESP8266WebServer.h>
  5. #include <SimpleTimer.h>
  6. #include <TimeLib.h> //https://github.com/PaulStoffregen/Time
  7. #include <ntp_time.h>
  8. #include <circular_log.h>
  9. #include <WiFiClientSecure.h>
  10. #include <PubSubClient.h>
  11. extern const unsigned char caCert[] PROGMEM;
  12. extern const unsigned int caCertLen;
  13. //IMPORTANT: Specify your WIFI settings:
  14. //#define WIFI_SSID "NOS-3B26" // setubal
  15. //#define WIFI_PASS "RMKSX2GL" // Setubal
  16. #define WIFI_SSID "MEO-AA9030"// Andre
  17. #define WIFI_PASS "81070ce635" // andre
  18. //IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):
  19. //#define ENABLE_MQTT
  20. #ifdef ENABLE_MQTT
  21. //NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.
  22. //NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc"
  23. //#define MQTT_SERVER "cc42fcb4f1eb492fa3c2e07a9e617830.s2.eu.hivemq.cloud" //"192.168.1.123"
  24. #define MQTT_SERVER "192.168.1.123"
  25. #define MQTT_PORT 1883 // 8883
  26. #define MQTT_USER "tezmqtt"
  27. #define MQTT_PASSWORD "mqtTez_123"
  28. #define MQTT_TOPIC_ROOT "home/grid_battery/" //this is where mqtt data will be pushed
  29. #define MQTT_PUSH_FREQ_SEC 2 //maximum mqtt update frequency in seconds
  30. #endif ENABLE_MQTT
  31. WiFiClient espClient;
  32. //WiFiClientSecure espClient;
  33. PubSubClient mqttClient(espClient);
  34. IPAddress thisip;
  35. ESP8266WebServer server(80);
  36. SimpleTimer timer;
  37. char g_szRecvBuff[7000];
  38. circular_log<7000> g_log;
  39. bool ntpTimeReceived = false;
  40. int g_baudRate = 0;
  41. void Log(const char* msg)
  42. {
  43. g_log.Log(msg);
  44. }
  45. int LEDPIN = 2; // The on-board Wemos D1 mini LED
  46. //////////////////////////////////////
  47. void setup() {
  48. Serial.begin(115200);
  49. Serial.println("---");
  50. Serial.println("Serial started");
  51. pinMode(LED_BUILTIN, OUTPUT);
  52. digitalWrite(LED_BUILTIN, LOW); // LOW = on
  53. delay(2000);
  54. digitalWrite(LED_BUILTIN, HIGH); // HIGH = off
  55. // connect to WiFi
  56. WiFi.mode(WIFI_STA);
  57. WiFi.persistent(false); //our credentials are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)
  58. WiFi.hostname("PylonBattery");
  59. Serial.println();
  60. Serial.print("connecting to ");
  61. Serial.println(WIFI_SSID);
  62. WiFi.begin(WIFI_SSID, WIFI_PASS);
  63. while (WiFi.status() != WL_CONNECTED) {
  64. delay(500);
  65. Serial.print(".");
  66. }
  67. Serial.println("");
  68. Serial.println("WiFi connected");
  69. Serial.println("IP address: ");
  70. Serial.println(WiFi.localIP());
  71. Serial.println("");
  72. for(int i=1; i<=3; i++){
  73. LedBlink();
  74. }
  75. ArduinoOTA.setHostname("AndrePylon");
  76. ArduinoOTA.begin();
  77. server.on("/", handleRoot);
  78. server.on("/log", handleLog);
  79. server.on("/req", handleReq);
  80. server.on("/jsonOut", handleJsonOut);
  81. server.on("/reboot", [](){
  82. ESP.restart();
  83. });
  84. server.begin();
  85. Serial.println("web server started");
  86. Serial.println("");
  87. delay(1000);
  88. syncTime();
  89. Serial.println("SNTP time synced");
  90. Serial.println("");
  91. for(int i=1; i<=3; i++){
  92. LedBlink();
  93. }
  94. /* COMMENTED BY TEZ */
  95. /*
  96. // https://links2004.github.io/Arduino/d2/d2f/class_wi_fi_client_secure.html
  97. // Load root certificate in DER format into WiFiClientSecure object
  98. // bool res = espClient.setCACert_P(caCert, caCertLen);
  99. bool res = espClient.setCertificate(caCert, caCertLen);
  100. if (!res) {
  101. Serial.println("Failed to load root CA certificate!");
  102. while (true) {
  103. yield();
  104. }
  105. }
  106. Serial.println("local load root CA certificate OK!");
  107. // VERIFY SSL FINGERPRINT
  108. 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") ) {
  109. Serial.println( "Fingerprint certificate NOT verified sorry!" );
  110. // mqttClient.disconnect();
  111. // return false;
  112. }else{
  113. Serial.println( "Fingerprint verified OK!");
  114. }
  115. */
  116. // END COMMENTED BY TEZ
  117. #ifdef ENABLE_MQTT
  118. mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
  119. #endif
  120. Log("Boot event");
  121. }
  122. //////////////////////////////////////////////////
  123. void LedBlink(){
  124. digitalWrite(LED_BUILTIN, LOW);
  125. delay(150);
  126. digitalWrite(LED_BUILTIN, HIGH); // high = off
  127. delay(150);
  128. }
  129. //////////////////////////////////////////////////
  130. void handleLog()
  131. {
  132. server.send(200, "text/html", g_log.c_str());
  133. }
  134. //////////////////////////////////////////////////
  135. void switchBaud(int newRate)
  136. {
  137. if(g_baudRate == newRate)
  138. {
  139. return;
  140. }
  141. if(g_baudRate != 0)
  142. {
  143. Serial.flush();
  144. delay(20);
  145. Serial.end();
  146. delay(20);
  147. }
  148. char szMsg[50];
  149. snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate);
  150. Log(szMsg);
  151. Serial.begin(newRate);
  152. g_baudRate = newRate;
  153. delay(20);
  154. }
  155. //////////////////////////////////////////////////
  156. void waitForSerial()
  157. {
  158. for(int ix=0; ix<150;ix++)
  159. {
  160. if(Serial.available()) break;
  161. delay(10);
  162. }
  163. }
  164. //////////////////////////////////////////////////
  165. int readFromSerial()
  166. {
  167. memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));
  168. int recvBuffLen = 0;
  169. bool foundTerminator = true;
  170. waitForSerial();
  171. while(Serial.available())
  172. {
  173. char szResponse[256] = "";
  174. const int readNow = Serial.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end)
  175. if(readNow > 0 &&
  176. szResponse[0] != '\0')
  177. {
  178. if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff)))
  179. {
  180. Log("WARNING: Read too much data on the console!");
  181. break;
  182. }
  183. strcat(g_szRecvBuff, szResponse);
  184. recvBuffLen += readNow;
  185. if(strstr(g_szRecvBuff, "$$\r\n\rpylon"))
  186. {
  187. strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add
  188. foundTerminator = true;
  189. break; //found end of the string
  190. }
  191. if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit"))
  192. {
  193. //we need to send new line character so battery continues the output
  194. Serial.write("\r");
  195. }
  196. waitForSerial();
  197. }
  198. }
  199. if(recvBuffLen > 0 )
  200. {
  201. if(foundTerminator == false)
  202. {
  203. Log("Failed to find pylon> terminator");
  204. }
  205. }
  206. return recvBuffLen;
  207. }
  208. //////////////////////////////////////////////////
  209. bool readFromSerialAndSendResponse()
  210. {
  211. const int recvBuffLen = readFromSerial();
  212. if(recvBuffLen > 0)
  213. {
  214. server.sendContent(g_szRecvBuff);
  215. return true;
  216. }
  217. return false;
  218. }
  219. //////////////////////////////////////////////////
  220. bool sendCommandAndReadSerialResponse(const char* pszCommand)
  221. {
  222. switchBaud(115200);
  223. if(pszCommand[0] != '\0')
  224. {
  225. Serial.write(pszCommand);
  226. }
  227. Serial.write("\n");
  228. const int recvBuffLen = readFromSerial();
  229. if(recvBuffLen > 0)
  230. {
  231. return true;
  232. }
  233. //wake up console and try again:
  234. wakeUpConsole();
  235. if(pszCommand[0] != '\0')
  236. {
  237. Serial.write(pszCommand);
  238. }
  239. Serial.write("\n");
  240. return readFromSerial() > 0;
  241. }
  242. //////////////////////////////////////////////////
  243. void handleReq()
  244. {
  245. bool respOK;
  246. if(server.hasArg("code") == false)
  247. {
  248. respOK = sendCommandAndReadSerialResponse("");
  249. }
  250. else
  251. {
  252. respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str());
  253. }
  254. if(respOK)
  255. {
  256. server.send(200, "text/plain", g_szRecvBuff);
  257. }
  258. else
  259. {
  260. server.send(500, "text/plain", "????");
  261. }
  262. }
  263. //////////////////////////////////////////////////
  264. void handleJsonOut()
  265. {
  266. if(sendCommandAndReadSerialResponse("pwr") == false)
  267. {
  268. server.send(500, "text/plain", "Failed to get response to 'pwr' command");
  269. return;
  270. }
  271. parsePwrResponse(g_szRecvBuff);
  272. prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));
  273. server.send(200, "application/json", g_szRecvBuff);
  274. }
  275. //////////////////////////////////////////////////
  276. void handleRoot() {
  277. unsigned long days = 0, hours = 0, minutes = 0;
  278. unsigned long val = os_getCurrentTimeSec();
  279. days = val / (3600*24);
  280. val -= days * (3600*24);
  281. hours = val / 3600;
  282. val -= hours * 3600;
  283. minutes = val / 60;
  284. val -= minutes*60;
  285. static char szTmp[2500] = "";
  286. snprintf(szTmp, sizeof(szTmp)-1, "<html><b>Garage Battery</b><br>Time GMT: %d/%02d/%02d %02d:%02d:%02d (%s)<br>Uptime: %02d:%02d:%02d.%02d<br><br>free heap: %u<br>Wifi RSSI: %d<BR>Wifi SSID: %s",
  287. year(), month(), day(), hour(), minute(), second(), "GMT",
  288. (int)days, (int)hours, (int)minutes, (int)val,
  289. ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str());
  290. strncat(szTmp, "<BR><a href='/log'>Runtime log</a><HR>", sizeof(szTmp)-1);
  291. strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'></form><a href='/req?code=pwr'>Power</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a>", sizeof(szTmp)-1);
  292. strncat(szTmp, "</html>", sizeof(szTmp)-1);
  293. server.send(200, "text/html", szTmp);
  294. }
  295. unsigned long os_getCurrentTimeSec()
  296. {
  297. static unsigned int wrapCnt = 0;
  298. static unsigned long lastVal = 0;
  299. unsigned long currentVal = millis();
  300. if(currentVal < lastVal)
  301. {
  302. wrapCnt++;
  303. }
  304. lastVal = currentVal;
  305. unsigned long seconds = currentVal/1000;
  306. //millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter
  307. return (wrapCnt*4294967) + seconds;
  308. }
  309. //////////////////////////////////////////////////
  310. void syncTime()
  311. {
  312. // configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  313. //get time from NTP
  314. time_t currentTimeGMT = getNtpTime();
  315. if(currentTimeGMT)
  316. {
  317. ntpTimeReceived = true;
  318. setTime(currentTimeGMT);
  319. }
  320. else
  321. {
  322. timer.setTimeout(3000, syncTime); //try again in 5 seconds
  323. }
  324. struct tm timeinfo;
  325. gmtime_r(&currentTimeGMT, &timeinfo);
  326. Serial.print("Current time: ");
  327. Serial.print(asctime(&timeinfo));
  328. Serial.println("------------------");
  329. // Synchronize time using SNTP. This is necessary to verify that
  330. // the TLS certificates offered by the server are currently valid.
  331. // Serial.print("Setting time using SNTP");
  332. // configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov");
  333. // time_t now = time(nullptr);
  334. // while (now < 8 * 3600 * 2) {
  335. // delay(500);
  336. // Serial.print(".");
  337. // now = time(nullptr);
  338. // }
  339. //
  340. // setTime(now);
  341. //
  342. // Serial.println("");
  343. // struct tm timeinfo;
  344. // gmtime_r(&now, &timeinfo);
  345. // Serial.print("Current time: ");
  346. // Serial.print(asctime(&timeinfo));
  347. }
  348. //////////////////////////////////////////////////
  349. void wakeUpConsole()
  350. {
  351. switchBaud(1200);
  352. //byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};
  353. //Serial.write(wakeUpBuff, sizeof(wakeUpBuff));
  354. Serial.write("~20014682C0048520FCC3\r");
  355. delay(1000);
  356. byte newLineBuff[] = {0x0E, 0x0A};
  357. switchBaud(115200);
  358. for(int ix=0; ix<10; ix++)
  359. {
  360. Serial.write(newLineBuff, sizeof(newLineBuff));
  361. delay(1000);
  362. if(Serial.available())
  363. {
  364. while(Serial.available())
  365. {
  366. Serial.read();
  367. }
  368. break;
  369. }
  370. }
  371. }
  372. #define MAX_PYLON_BATTERIES 2
  373. struct pylonBattery
  374. {
  375. bool isPresent;
  376. long soc; //Coulomb in %
  377. long voltage; //in mW
  378. long current; //in mA, negative value is discharge
  379. long tempr; //temp of case or BMS?
  380. long cellTempLow;
  381. long cellTempHigh;
  382. long cellVoltLow;
  383. long cellVoltHigh;
  384. char baseState[9]; //Charge | Dischg | Idle
  385. char voltageState[9]; //Normal
  386. char currentState[9]; //Normal
  387. char tempState[9]; //Normal
  388. char time[20]; //2019-06-08 04:00:29
  389. char b_v_st[9]; //Normal (battery voltage?)
  390. char b_t_st[9]; //Normal (battery temperature?)
  391. bool isCharging() const { return strcmp(baseState, "Charge") == 0; }
  392. bool isDischarging() const { return strcmp(baseState, "Dischg") == 0; }
  393. bool isIdle() const { return strcmp(baseState, "Idle") == 0; }
  394. bool isBalancing() const { return strcmp(baseState, "Balance") == 0; }
  395. bool isNormal() const
  396. {
  397. if(isCharging() == false &&
  398. isDischarging() == false &&
  399. isIdle() == false &&
  400. isBalancing() == false)
  401. {
  402. return false; //base state looks wrong!
  403. }
  404. return strcmp(voltageState, "Normal") == 0 &&
  405. strcmp(currentState, "Normal") == 0 &&
  406. strcmp(tempState, "Normal") == 0 &&
  407. strcmp(b_v_st, "Normal") == 0 &&
  408. strcmp(b_t_st, "Normal") == 0 ;
  409. }
  410. };
  411. struct batteryStack
  412. {
  413. int batteryCount;
  414. int soc; //in %, if charging: average SOC, otherwise: lowest SOC
  415. int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest
  416. long currentDC; //mAh current going in or out of the battery
  417. long avgVoltage; //in mV
  418. char baseState[9]; //Charge | Dischg | Idle | Balance | Alarm!
  419. pylonBattery batts[MAX_PYLON_BATTERIES];
  420. bool isNormal() const
  421. {
  422. for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
  423. {
  424. if(batts[ix].isPresent &&
  425. batts[ix].isNormal() == false)
  426. {
  427. return false;
  428. }
  429. }
  430. return true;
  431. }
  432. //in wH
  433. long getPowerDC() const
  434. {
  435. return (long)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
  436. }
  437. //wH estimated current on AC side (taking into account Sofar ME3000SP losses)
  438. long getEstPowerAc() const
  439. {
  440. double powerDC = (double)getPowerDC();
  441. if(powerDC == 0)
  442. {
  443. return 0;
  444. }
  445. else if(powerDC < 0)
  446. {
  447. //we are discharging, on AC side we will see less power due to losses
  448. if(powerDC < -1000)
  449. {
  450. return (long)(powerDC*0.94);
  451. }
  452. else if(powerDC < -600)
  453. {
  454. return (long)(powerDC*0.90);
  455. }
  456. else
  457. {
  458. return (long)(powerDC*0.87);
  459. }
  460. }
  461. else
  462. {
  463. //we are charging, on AC side we will have more power due to losses
  464. if(powerDC > 1000)
  465. {
  466. return (long)(powerDC*1.06);
  467. }
  468. else if(powerDC > 600)
  469. {
  470. return (long)(powerDC*1.1);
  471. }
  472. else
  473. {
  474. return (long)(powerDC*1.13);
  475. }
  476. }
  477. }
  478. };
  479. batteryStack g_stack;
  480. //////////////////////////////////////////////////
  481. long extractInt(const char* pStr, int pos)
  482. {
  483. return atol(pStr+pos);
  484. }
  485. //////////////////////////////////////////////////
  486. void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)
  487. {
  488. strOut[strOutSize-1] = '\0';
  489. strncpy(strOut, pStr+pos, strOutSize-1);
  490. strOutSize--;
  491. //trim right
  492. while(strOutSize > 0)
  493. {
  494. if(isspace(strOut[strOutSize-1]))
  495. {
  496. strOut[strOutSize-1] = '\0';
  497. }
  498. else
  499. {
  500. break;
  501. }
  502. strOutSize--;
  503. }
  504. }
  505. /* Output has mixed \r and \r\n
  506. pwr
  507. @
  508. Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St
  509. 1 49735 -1440 22000 19000 19000 3315 3317 Dischg Normal Normal Normal 93% 2019-06-08 04:00:30 Normal Normal
  510. ....
  511. 8 - - - - - - - Absent - - - - - - -
  512. Command completed successfully
  513. $$
  514. pylon
  515. */
  516. //////////////////////////////////////////////////
  517. bool parsePwrResponse(const char* pStr)
  518. {
  519. if(strstr(pStr, "Command completed successfully") == NULL)
  520. {
  521. return false;
  522. }
  523. int chargeCnt = 0;
  524. int dischargeCnt = 0;
  525. int idleCnt = 0;
  526. int alarmCnt = 0;
  527. int socAvg = 0;
  528. int socLow = 0;
  529. int tempHigh = 0;
  530. int tempLow = 0;
  531. memset(&g_stack, 0, sizeof(g_stack));
  532. for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
  533. {
  534. char szToFind[32] = "";
  535. snprintf(szToFind, sizeof(szToFind)-1, "\r\r\n%d ", ix+1);
  536. const char* pLineStart = strstr(pStr, szToFind);
  537. if(pLineStart == NULL)
  538. {
  539. return false;
  540. }
  541. pLineStart += 3; //move past \r\r\n
  542. extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));
  543. if(strcmp(g_stack.batts[ix].baseState, "Absent") == 0)
  544. {
  545. g_stack.batts[ix].isPresent = false;
  546. }
  547. else
  548. {
  549. g_stack.batts[ix].isPresent = true;
  550. extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));
  551. extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));
  552. extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));
  553. extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));
  554. extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));
  555. extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));
  556. g_stack.batts[ix].voltage = extractInt(pLineStart, 6);
  557. g_stack.batts[ix].current = extractInt(pLineStart, 13);
  558. g_stack.batts[ix].tempr = extractInt(pLineStart, 20);
  559. g_stack.batts[ix].cellTempLow = extractInt(pLineStart, 27);
  560. g_stack.batts[ix].cellTempHigh = extractInt(pLineStart, 34);
  561. g_stack.batts[ix].cellVoltLow = extractInt(pLineStart, 41);
  562. g_stack.batts[ix].cellVoltHigh = extractInt(pLineStart, 48);
  563. g_stack.batts[ix].soc = extractInt(pLineStart, 91);
  564. //////////////////////////////// Post-process ////////////////////////
  565. g_stack.batteryCount++;
  566. g_stack.currentDC += g_stack.batts[ix].current;
  567. g_stack.avgVoltage += g_stack.batts[ix].voltage;
  568. socAvg += g_stack.batts[ix].soc;
  569. if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }
  570. else if(g_stack.batts[ix].isCharging()){chargeCnt++;}
  571. else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}
  572. else if(g_stack.batts[ix].isIdle()){idleCnt++;}
  573. else{ alarmCnt++; } //should not really happen!
  574. if(g_stack.batteryCount == 1)
  575. {
  576. socLow = g_stack.batts[ix].soc;
  577. tempLow = g_stack.batts[ix].cellTempLow;
  578. tempHigh = g_stack.batts[ix].cellTempHigh;
  579. }
  580. else
  581. {
  582. if(socLow > g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}
  583. if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}
  584. if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}
  585. }
  586. }
  587. }
  588. //now update stack state:
  589. g_stack.avgVoltage /= g_stack.batteryCount;
  590. g_stack.soc = socLow;
  591. if(tempHigh > 15000) //15C
  592. {
  593. g_stack.temp = tempHigh; //in the summer we highlight the warmest cell
  594. }
  595. else
  596. {
  597. g_stack.temp = tempLow; //in the winter we focus on coldest cell
  598. }
  599. if(alarmCnt > 0)
  600. {
  601. strcpy(g_stack.baseState, "Alarm!");
  602. }
  603. else if(chargeCnt == g_stack.batteryCount)
  604. {
  605. strcpy(g_stack.baseState, "Charge");
  606. g_stack.soc = (int)(socAvg / g_stack.batteryCount);
  607. }
  608. else if(dischargeCnt == g_stack.batteryCount)
  609. {
  610. strcpy(g_stack.baseState, "Dischg");
  611. }
  612. else if(idleCnt == g_stack.batteryCount)
  613. {
  614. strcpy(g_stack.baseState, "Idle");
  615. }
  616. else
  617. {
  618. strcpy(g_stack.baseState, "Balance");
  619. }
  620. return true;
  621. }
  622. //////////////////////////////////////////////////
  623. void prepareJsonOutput(char* pBuff, int buffSize)
  624. {
  625. memset(pBuff, 0, buffSize);
  626. 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,
  627. g_stack.temp, g_stack.isNormal() ? "true" : "false");
  628. }
  629. //////////////////////////////////////////////////
  630. void loop() {
  631. #ifdef ENABLE_MQTT
  632. mqttLoop();
  633. #endif
  634. ArduinoOTA.handle();
  635. server.handleClient();
  636. timer.run();
  637. //if there are bytes availbe on serial here - it's unexpected
  638. //when we send a command to battery, we read whole response
  639. //if we get anything here anyways - we will log it
  640. int bytesAv = Serial.available();
  641. if(bytesAv > 0)
  642. {
  643. if(bytesAv > 63)
  644. {
  645. bytesAv = 63;
  646. }
  647. char buff[64+4] = "RCV:";
  648. if(Serial.readBytes(buff+4, bytesAv) > 0)
  649. {
  650. digitalWrite(LED_BUILTIN, LOW);
  651. delay(5);
  652. digitalWrite(LED_BUILTIN, HIGH);//high is off
  653. Log(buff);
  654. }
  655. }
  656. }
  657. //////////////////////////////////////////////////
  658. #ifdef ENABLE_MQTT
  659. #define ABS_DIFF(a, b) (a > b ? a-b : b-a)
  660. //////////////////////////////////////////////////
  661. void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)
  662. {
  663. char szTmp[16] = "";
  664. snprintf(szTmp, 15, "%.2f", newValue);
  665. if(force || ABS_DIFF(newValue, oldValue) > minDiff)
  666. {
  667. mqttClient.publish(topic, szTmp, false);
  668. }
  669. }
  670. //////////////////////////////////////////////////
  671. void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)
  672. {
  673. char szTmp[16] = "";
  674. snprintf(szTmp, 15, "%d", newValue);
  675. if(force || ABS_DIFF(newValue, oldValue) > minDiff)
  676. {
  677. mqttClient.publish(topic, szTmp, false);
  678. }
  679. }
  680. //////////////////////////////////////////////////
  681. void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)
  682. {
  683. if(force || strcmp(newValue, oldValue) != 0)
  684. {
  685. mqttClient.publish(topic, newValue, false);
  686. }
  687. }
  688. //////////////////////////////////////////////////
  689. void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */)
  690. {
  691. Serial.println("entered publish");
  692. mqtt_publish_f(MQTT_TOPIC_ROOT "soc", g_stack.soc, lastSentData.soc, 0, forceUpdate);
  693. mqtt_publish_f(MQTT_TOPIC_ROOT "temp", (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0, forceUpdate);
  694. mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC", g_stack.getEstPowerAc(), lastSentData.getEstPowerAc(), 10, forceUpdate);
  695. mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount, lastSentData.batteryCount, 0, forceUpdate);
  696. mqtt_publish_s(MQTT_TOPIC_ROOT "base_state", g_stack.baseState, lastSentData.baseState , forceUpdate);
  697. mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal", g_stack.isNormal() ? 1:0, lastSentData.isNormal() ? 1:0, 0, forceUpdate);
  698. Serial.println("finished publish");
  699. }
  700. //////////////////////////////////////////////////
  701. void mqttLoop()
  702. {
  703. //if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)
  704. static unsigned long g_lastConnectionAttempt = 0;
  705. //first: let's make sure we are connected to mqtt
  706. const char* topicLastWill = MQTT_TOPIC_ROOT "availability";
  707. if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) {
  708. if(mqttClient.connect("GarageBattery", MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline"))
  709. {
  710. Log("Connected to MQTT server: " MQTT_SERVER);
  711. Serial.println("Connected to MQTT server!!!");
  712. mqttClient.publish(topicLastWill, "online", true);
  713. // test by TeZ
  714. mqttClient.publish("home/grid_battery/tez", "cicciobello", true);
  715. mqttClient.publish("testbytez", "232323", true);
  716. }
  717. else
  718. {
  719. Log("Failed to connect to MQTT server.");
  720. Serial.println("Failed to connect to MQTT server.");
  721. }
  722. g_lastConnectionAttempt = os_getCurrentTimeSec();
  723. }
  724. //next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)
  725. static unsigned long g_lastDataSent = 0;
  726. if(mqttClient.connected() &&
  727. os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC &&
  728. sendCommandAndReadSerialResponse("pwr") == true)
  729. {
  730. Serial.println("Connected and sending to MQTT server!!!");
  731. static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again
  732. static unsigned int callCnt = 0;
  733. parsePwrResponse(g_szRecvBuff);
  734. bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call
  735. pushBatteryDataToMqtt(lastSentData, forceUpdate);
  736. callCnt++;
  737. g_lastDataSent = os_getCurrentTimeSec();
  738. memcpy(&lastSentData, &g_stack, sizeof(batteryStack));
  739. }
  740. mqttClient.loop();
  741. }
  742. #endif //ENABLE_MQTT