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.

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