{"id":8337,"date":"2025-02-10T20:35:48","date_gmt":"2025-02-10T19:35:48","guid":{"rendered":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/?p=8337"},"modified":"2025-12-16T15:09:10","modified_gmt":"2025-12-16T14:09:10","slug":"lan-fuer-pylontech-pv-akkustatus-opendtu-und-mehr-im-homeassistant","status":"publish","type":"post","link":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/2025\/02\/10\/lan-fuer-pylontech-pv-akkustatus-opendtu-und-mehr-im-homeassistant\/","title":{"rendered":"LAN f\u00fcr Pylontech PV-Akkustatus, OpenDTU und mehr im HomeAssistant"},"content":{"rendered":"<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_8337\" class=\"pvc_stats all  \" data-element-id=\"8337\" style=\"\"><i class=\"pvc-stats-icon medium\" aria-hidden=\"true\"><svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"far\" data-icon=\"chart-bar\" role=\"img\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 512 512\" class=\"svg-inline--fa fa-chart-bar fa-w-16 fa-2x\"><path fill=\"currentColor\" d=\"M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z\" class=\"\"><\/path><\/svg><\/i> <img decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif\" border=0 \/><\/p>\n<div class=\"pvc_clear\"><\/div>\n<p>&nbsp;<\/p>\n<audio class=\"wp-audio-shortcode\" id=\"audio-8337-1\" preload=\"none\" style=\"width: 100%;\" controls=\"controls\"><source type=\"audio\/mpeg\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/LAN-Platine_baendigt_WLAN-Chaos_im_Smarthome.m4a?_=1\" \/><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/LAN-Platine_baendigt_WLAN-Chaos_im_Smarthome.m4a\">http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/LAN-Platine_baendigt_WLAN-Chaos_im_Smarthome.m4a<\/a><\/audio>\n<p><em>Ein KI-generierter Podcastbeitrag zum Bloginhalt&#8230;<\/em><\/p>\n<p style=\"text-align: justify\">In dem Beitrag <strong><a href=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/2024\/02\/06\/pylontech-pv-akkustatus-im-homeassistant\/\" aria-label=\"\u201ePylontech PV-Akkustatus im HomeAssistant\u201c (Bearbeiten)\">Pylontech PV-Akkustatus im HomeAssistant<\/a><\/strong> hatte ich das Projekt &#8222;Pylontech-Battery-Monitoring&#8220; der folgenden GitHub Links etwas aufgeh\u00fcbscht und eine Platine gezeichnet, um das ganze Konstrukt etwas kompakter und professioneller aufzubereiten.&nbsp;<br \/>\n<a href=\"https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\">https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring<\/a><br \/>\n<a href=\"https:\/\/github.com\/hidaba\/PylontechMonitoring\">https:\/\/github.com\/hidaba\/PylontechMonitoring<\/a><br \/>\nIm Homeassistant wurden damit, bzw. werden, s\u00e4mtliche Batteriedaten der Pylontech Akku Module angezeigt. Super! Wenn ich aber einen Blick in die Ger\u00e4teliste der, in meinen Wifi &#8211; Netzen angemeldeten Ger\u00e4te ansehe, wird mir fast \u00fcbel &#8211; es sind mittlerweile viel zu viele Funkger\u00e4te, vor allem aus dem SmartHome Bereich, die sich die Kanalbandbreite teilen.&nbsp; So ist es mein aktueller Plan, einige der Smarthome Ger\u00e4te ins verkabelte LAN-Netzwerk zu bringen.<\/p>\n<p>edit 06.Mar.2025: das Projekt ist jetzt auch unter:<br \/>\n<a href=\"https:\/\/github.com\/ingmarsretro\/pylontech_Lan_Interface\">https:\/\/github.com\/ingmarsretro\/pylontech_Lan_Interface<\/a><\/p>\n<p>zu finden.<\/p>\n<p style=\"text-align: justify\">Die selber gebastelten Ger\u00e4te auf Basis der ESPs bieten sich daf\u00fcr an. Das sind dann Ger\u00e4te, wie das OpenDTU Interface, das EVU-Smarthomeinterface oder wie hier, die Schnittstelle von der seriellen Console der Pylontechakkus zum MQTT Server im Homeassistant.<\/p>\n<p style=\"text-align: justify\">Dazu habe ich einige Versuche mit Boards wie dem OLIMEX ESP32-PoE und dem WT32-ETH01 gemacht. Die Olimexplatine h\u00e4tte den gro\u00dfen Vorteil, auch \u00fcber PoE mit Energie versorgt werden zu k\u00f6nnen. Allerdings ist die Spannungsversorgung bei PoE Betrieb so &#8222;schlecht&#8220;, dass die ben\u00f6tigten Standards externer Boards nicht erf\u00fcllt werden. Hier kann ich zum Beispiel das Funkmodul NRF24L01 erw\u00e4hnen. Damit habe ich einige Versuche gemacht und beschlossen, die PoE Funktionalit\u00e4t vorerst einmal au\u00dfer Acht zu lassen. So entstand dann der Plan mit dem WT32-ETH01 auf dem ein ESP32 arbeitet, ein universelles Board zu designen, auf dem mehrere Schnittstellen vorhanden sind. Es sollte folgendes k\u00f6nnen:<\/p>\n<ul>\n<li>mit OpenDTU und&nbsp;NRF24L01 mit den PV-Invertern kommunizieren<\/li>\n<li>\u00fcber die Pylontech Console via MQTT mit dem Smarthome System kommunizieren<\/li>\n<li>eine optionale CAN Schnittstelle besitzen<\/li>\n<li>neben der RS232 Schnittstelle auch \u00fcber RS422\/RS485 kommunizieren zu k\u00f6nnen<\/li>\n<li>die Spannungsversorgung \u00fcber 5V USB zu erhalten<\/li>\n<li>und alles sch\u00f6n klein und kompakt in einem Geh\u00e4use verpackt zu haben<\/li>\n<\/ul>\n<p style=\"text-align: justify\">So habe ich dazu eine Schaltung entworfen und eine Platine gezeichnet. Bei einer Fernost PCB Manufaktur lie\u00df ich die Boards fertigen. Das Best\u00fccken ist auch schnell erledigt.<\/p>\n<div id=\"attachment_8360\" style=\"width: 484px\" class=\"wp-caption alignnone\"><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a.png\"><img fetchpriority=\"high\" decoding=\"async\" aria-describedby=\"caption-attachment-8360\" class=\"wp-image-8360 size-large\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a-1024x706.png\" alt=\"\" width=\"474\" height=\"327\" srcset=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a-1024x706.png 1024w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a-300x207.png 300w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a-768x529.png 768w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a-1536x1058.png 1536w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/schematic_pcb1a-2048x1411.png 2048w\" sizes=\"(max-width: 474px) 100vw, 474px\" \/><\/a><p id=\"caption-attachment-8360\" class=\"wp-caption-text\">Schaltplan des Universal Lan Interface<\/p><\/div>\n<p>Das nachstehende Bild zeigt das Platinenlayout vor der Fertigung.<\/p>\n<p><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a.png\"><img decoding=\"async\" class=\"alignnone wp-image-8362 size-large\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a-e1739183320710-1024x966.png\" alt=\"\" width=\"474\" height=\"447\" srcset=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a-e1739183320710-1024x966.png 1024w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a-e1739183320710-300x283.png 300w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a-e1739183320710-768x725.png 768w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a-e1739183320710-1536x1449.png 1536w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/pylontec_mqttinterface1a-e1739183320710-2048x1932.png 2048w\" sizes=\"(max-width: 474px) 100vw, 474px\" \/><\/a><\/p>\n<p style=\"text-align: justify\">Das WT32-ETH01 Board besitzt keinen USB-Port zum Programmieren des Controllers. Es wird \u00fcber einen externen USB-UART Adapter programmiert. Um den Programmiermodus zu aktivieren, muss auch ein IO Pin gegen GND geschaltet werden. Um das etwas zu vereinfachen, gibt es jetzt auf dem Board einen Jumper &#8222;PROG&#8220;. Wenn diese Br\u00fccke gesteckt ist, kann der der WT32 die Firmwarefiles empfangen. Einen Pinheader Steckplatz &#8222;TO-FTDI&#8220; habe ich als Anschlussm\u00f6glichkeit f\u00fcr den USB-UART Adapter vorgesehen.<br \/>\nDie Platine ist nun so konzipiert, dass man damit unterschiedliche Ger\u00e4te bedienen kann. Schliesst man ein NRF24L01 Modul an den Pinheader &#8222;NRF24L01+&#8220; an und flasht das ESP32-OpenDTU Image auf den Controller, dann k\u00f6nnen damit die Wechselrichterdaten empfangen und \u00fcber das LAN Netzwerk \u00fcbertragen werden. Ein geeignetes IO-config jason-file f\u00fcr die Benutzung des WT32 habe ich erstellt.<\/p>\n<p style=\"text-align: justify\">Eine weitere Anwendung ist die Verwendung der Platine mit der seriellen Ausgabe der Batteriedaten der Pylontech PV Akkus. Die Akkus stellen einen &#8222;Console&#8220; Port zur Verf\u00fcgung der eine RS232 Schnittstelle darstellt. \u00dcber diese werden die Daten zum WT32 Controller \u00fcbertragen und stehen dann \u00fcber LAN im lokalen Netzwerk zur Verf\u00fcgung.&nbsp;<\/p>\n<p style=\"text-align: justify\">Dazu habe ich das ESP8266 script von <a href=\"https:\/\/github.com\/hidaba\/PylontechMonitoring\">hidaba<\/a> und <a href=\"https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\">irekzielinski<\/a> f\u00fcr die ESP32 Controller angepasst. (siehe Code am Ende des Beitrags)<\/p>\n<p><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-8375 size-large\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50-1024x462.png\" alt=\"\" width=\"474\" height=\"214\" srcset=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50-1024x462.png 1024w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50-300x135.png 300w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50-768x346.png 768w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50-1536x692.png 1536w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/Screenshot_2025-02-10-13-55-01-89_c3a231c25ed346e59462e84656a70e50-2048x923.png 2048w\" sizes=\"(max-width: 474px) 100vw, 474px\" \/><\/a><\/p>\n<p style=\"text-align: justify\">Ist der Code kompiliert und hochgeladen, dann sollte nach Anschluss aller Verbindungen unter der eingestellten IP-Adresse der Status der Pylontec Batterien zu sehen sein.<\/p>\n<div id=\"attachment_8354\" style=\"width: 484px\" class=\"wp-caption alignnone\"><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled.jpg\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8354\" class=\"wp-image-8354 size-large\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled-e1739193679232-1024x958.jpg\" alt=\"\" width=\"474\" height=\"443\" srcset=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled-e1739193679232-1024x958.jpg 1024w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled-e1739193679232-300x281.jpg 300w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled-e1739193679232-768x719.jpg 768w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled-e1739193679232-1536x1437.jpg 1536w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_pcb-scaled-e1739193679232.jpg 1924w\" sizes=\"(max-width: 474px) 100vw, 474px\" \/><\/a><p id=\"caption-attachment-8354\" class=\"wp-caption-text\">Ausf\u00fchrung der Platine mit Pylontech Setup<\/p><\/div>\n<div id=\"attachment_8352\" style=\"width: 484px\" class=\"wp-caption alignnone\"><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled.jpg\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-8352\" class=\"wp-image-8352 size-large\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled-e1739193738476-1024x952.jpg\" alt=\"\" width=\"474\" height=\"441\" srcset=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled-e1739193738476-1024x952.jpg 1024w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled-e1739193738476-300x279.jpg 300w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled-e1739193738476-768x714.jpg 768w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled-e1739193738476-1536x1428.jpg 1536w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3d_case_photo_with_ant-scaled-e1739193738476.jpg 1710w\" sizes=\"(max-width: 474px) 100vw, 474px\" \/><\/a><p id=\"caption-attachment-8352\" class=\"wp-caption-text\">Ausf\u00fchrung mit OpenDTU und NRF24L01 Setup<\/p><\/div>\n<p style=\"text-align: justify\">Die im Bild dargestellten Ger\u00e4tesetups sind mit einem Geh\u00e4use ausgestattet. Die &#8222;.stl&#8220; Dateien dazu habe ich mit FreeCad erstellt auf thingiverse ver\u00f6ffentlicht.<\/p>\n<p><a href=\"https:\/\/www.thingiverse.com\/thing:6939824\">Link zu den thingiverse Dateien<\/a><\/p>\n<p><a href=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3dtool_screenshot.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-medium wp-image-8356\" src=\"http:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3dtool_screenshot-300x207.png\" alt=\"\" width=\"300\" height=\"207\" srcset=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3dtool_screenshot-300x207.png 300w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3dtool_screenshot-1024x706.png 1024w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3dtool_screenshot-768x530.png 768w, https:\/\/blog.fh-kaernten.at\/ingmarsretro\/files\/2025\/02\/3dtool_screenshot.png 1038w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>Hier der angepasste Code f\u00fcr die Verwendung mit dem WT32-ETH01 Board:<\/p>\n<p>Die Datei &#8222;TimerConfig.h&#8220; ist zu erstellen. Folgender Inhalt muss in der Datei stehen:<\/p>\n<p><em>\/\/ TimerConfig.h<\/em><br \/>\n<em>#ifndef TIMERCONFIG_H<\/em><br \/>\n<em>#define TIMERCONFIG_H<br \/>\n<\/em><em>#define TIMER_BASE_CLK 80000000<br \/>\n<\/em><em>#endif \/\/ TIMERCONFIG_H<\/em><\/p>\n<p>Ist die Datei angelegt, sollte sie im Verzeichnis des Hauptprogramms liegen. Hier das Hauptprogramm:<\/p>\n<pre style=\"font-family: arial;font-size: 12px;border: 1px dashed #CCCCCC;width: 99%;height: auto;overflow: auto;background: #f0f0f0;;background-image: url('https:\/\/blogger.googleusercontent.com\/img\/b\/R29vZ2xl\/AVvXsEioQnWdqp_vP8NPTOXAbVP-vR57MhJcY6EeOZrxToDvX_tHTHk4XMBatphK-fUR6F1Se7Sfn5z0Ps9pV6eme4bz4cHep-sfdsM7-qhx-Tbfuoa-pTlJ2TynXwSzDnN34aurZSXcB7bJ4h-s\/s320\/codebg.gif');padding: 0px;color: #000000;text-align: left;line-height: 20px\"><code style=\"color: #000000\"> \/\/Pylontec2MQTT interface  \r\n \/\/code by https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring  \r\n \/\/  and https:\/\/github.com\/hidaba\/PylontechMonitoring  \r\n \/\/ the original code used WEMOS Boards with ESP8266  \r\n \/\/ code changed to use with WT32-ETH01 Board for use with LAN connection  \r\n \/\/ changes by ingmarsretro 01\/2025  \r\n #include &lt;ETH.h&gt;  \r\n #include &lt;WiFi.h&gt;  \r\n #include &lt;ESPmDNS.h&gt;  \r\n #include &lt;ArduinoOTA.h&gt;  \r\n #include &lt;WebServer.h&gt;  \r\n #include &lt;circular_log.h&gt;  \r\n #include &lt;ArduinoJson.h&gt;  \r\n #include &lt;NTPClient.h&gt;  \r\n #include \"TimerConfig.h\"  \r\n #include &lt;ESP32TimerInterrupt.h&gt;  \r\n \/\/+++ START CONFIGURATION +++  \r\n #define LED 12  \r\n #define RXD2 5  \r\n #define TXD2 17  \r\n #define HOSTNAME \"mppsolar-pylontec\"  \r\n #define STATIC_IP  \r\n IPAddress local_IP(192, 168, xx, yy);  \r\n IPAddress subnet(255, 255, 255, 0);  \r\n IPAddress gateway(192, 168, yy, zz);  \r\n IPAddress primaryDNS(192, 168, yy, ww);  \r\n \/\/Uncomment for authentication page  \r\n \/\/#define AUTHENTICATION  \r\n const char* www_username = \"admin\";  \r\n const char* www_password = \"password\";  \r\n \/\/IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):  \r\n #define ENABLE_MQTT  \r\n \/\/ Set offset time in seconds to adjust for your timezone, for example:  \r\n \/\/ GMT +1 = 3600  \r\n \/\/ GMT 0 = 0  \r\n #define GMT 3600  \r\n \/\/NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.  \r\n \/\/NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example \"soc\" will be pushed to: \"home\/grid_battery\/soc\"  \r\n #define MQTT_SERVER    \"192.168.aa.bb\"  \r\n #define MQTT_PORT     1883  \r\n #define MQTT_USER     \"\"  \r\n #define MQTT_PASSWORD   \"\"  \r\n #define MQTT_TOPIC_ROOT  \"ingmarsretro\/pylontec\/\" \/\/this is where mqtt data will be pushed  \r\n #define MQTT_PUSH_FREQ_SEC 2 \/\/maximum mqtt update frequency in seconds  \r\n \/\/+++  END CONFIGURATION +++  \r\n #ifdef ENABLE_MQTT  \r\n #include &lt;PubSubClient.h&gt;  \r\n WiFiClient ethClient;  \r\n PubSubClient mqttClient(ethClient);  \r\n #endif \/\/ENABLE_MQTT  \r\n \/\/text response  \r\n char g_szRecvBuff[7000];  \r\n const long utcOffsetInSeconds = GMT;  \r\n char daysOfTheWeek[7][12] = {\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"};  \r\n \/\/ Define NTP Client to get time  \r\n WiFiUDP ntpUDP;  \r\n NTPClient timeClient(ntpUDP, \"pool.ntp.org\", utcOffsetInSeconds);  \r\n \/\/ESP8266WebServer server(80);  \r\n WebServer server(80);  \r\n circular_log&lt;7000&gt; g_log;  \r\n bool ntpTimeReceived = false;  \r\n int g_baudRate = 0;  \r\n void Log(const char* msg)  \r\n {  \r\n  g_log.Log(msg);  \r\n }  \r\n \/\/Define Interrupt Timer to Calculate Power meter every second (kWh)  \r\n #define USING_TIM_DIV1 true                       \/\/ for shortest and most accurate timer  \r\n \/\/ESP8266Timer ITimer;  \r\n ESP32Timer ITimer(0); \/\/ Use timer 0  \r\n bool setInterval(unsigned long interval, timer_callback callback);   \/\/ interval (in microseconds)  \r\n #define TIMER_INTERVAL_MS 1000  \r\n \/\/Global Variables for the Power Meter - accessible from the calculating interrupt und from main  \r\n unsigned long powerIN = 0;    \/\/WS gone in to the BAttery  \r\n unsigned long powerOUT = 0;   \/\/WS gone out of the Battery  \r\n \/\/Global Variables for the Power Meter - \u00dcberlauf  \r\n unsigned long powerINWh = 0;    \/\/WS gone in to the BAttery  \r\n unsigned long powerOUTWh = 0;   \/\/WS gone out of the Battery  \r\n void setup() {  \r\n  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); \/\/clean variable  \r\n  pinMode(LED, OUTPUT);   \r\n  \/\/digitalWrite(LED, HIGH);\/\/high is off  \r\n  digitalWrite(LED, LOW);\/\/low is off  \r\n  \/\/ put your setup code here, to run once:  \r\n  \/\/WiFi.mode(WIFI_STA);  \r\n  \/\/WiFi.persistent(false); \/\/our credentialss are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)  \r\n  \/\/WiFi.hostname(HOSTNAME);  \r\n  ETH.begin();  \r\n  ETH.setHostname(HOSTNAME);  \r\n   #ifdef STATIC_IP  \r\n    ETH.config(local_IP, gateway, subnet, primaryDNS);  \r\n  #endif  \r\n  for(int ix=0; ix&lt;10; ix++)  \r\n  {  \r\n   Log(\"Wait for LAN Connection\");  \r\n   if (ETH.linkUp()) {  \r\n     Serial2.println(\"Ethernet connected\");  \r\n     Serial2.print(\"IP Address: \");  \r\n     Serial2.println(ETH.localIP());  \r\n   } else {  \r\n     Serial2.println(\"Failed to connect to Ethernet\");  \r\n   }  \r\n   delay(1000);  \r\n  }  \r\n  ArduinoOTA.setHostname(HOSTNAME);  \r\n  ArduinoOTA.begin();  \r\n  server.on(\"\/\", handleRoot);  \r\n  server.on(\"\/log\", handleLog);  \r\n  server.on(\"\/req\", handleReq);  \r\n  server.on(\"\/jsonOut\", handleJsonOut);  \r\n  server.on(\"\/reboot\", [](){  \r\n   #ifdef AUTHENTICATION  \r\n   if (!server.authenticate(www_username, www_password)) {  \r\n    return server.requestAuthentication();  \r\n   }  \r\n   #endif  \r\n   ESP.restart();  \r\n  });  \r\n  server.begin();   \r\n  timeClient.begin();  \r\n #ifdef ENABLE_MQTT  \r\n  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);  \r\n #endif  \r\n  Log(\"Boot event\");  \r\n }  \r\n void handleLog()  \r\n {  \r\n  #ifdef AUTHENTICATION  \r\n  if (!server.authenticate(www_username, www_password)) {  \r\n   return server.requestAuthentication();  \r\n  }   \r\n  #endif  \r\n  server.send(200, \"text\/html\", g_log.c_str());  \r\n }  \r\n void switchBaud(int newRate)  \r\n {  \r\n  if(g_baudRate == newRate)  \r\n  {  \r\n   return;  \r\n  }  \r\n  if(g_baudRate != 0)  \r\n  {  \r\n   Serial2.flush();  \r\n   delay(20);  \r\n   Serial2.end();  \r\n   delay(20);  \r\n  }  \r\n  char szMsg[50];  \r\n  snprintf(szMsg, sizeof(szMsg)-1, \"New baud: %d\", newRate);  \r\n  Log(szMsg);  \r\n  Serial2.begin(newRate,SERIAL_8N1, RXD2, TXD2);  \r\n  g_baudRate = newRate;  \r\n  delay(20);  \r\n }  \r\n void waitForSerial()  \r\n {  \r\n  for(int ix=0; ix&lt;150;ix++)  \r\n  {  \r\n   if(Serial2.available()) break;  \r\n   delay(10);  \r\n  }  \r\n }  \r\n int readFromSerial()  \r\n {  \r\n  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));  \r\n  int recvBuffLen = 0;  \r\n  bool foundTerminator = true;  \r\n  waitForSerial();  \r\n  while(Serial2.available())  \r\n  {  \r\n   char szResponse[256] = \"\";  \r\n   const int readNow = Serial2.readBytesUntil('&gt;', szResponse, sizeof(szResponse)-1); \/\/all commands terminate with \"$$\\r\\n\\rpylon&gt;\" (no new line at the end)  \r\n   if(readNow &gt; 0 &amp;&amp;   \r\n     szResponse[0] != '\\0')  \r\n   {  \r\n    if(readNow + recvBuffLen + 1 &gt;= (int)(sizeof(g_szRecvBuff)))  \r\n    {  \r\n     Log(\"WARNING: Read too much data on the console!\");  \r\n     break;  \r\n    }  \r\n    strcat(g_szRecvBuff, szResponse);  \r\n    recvBuffLen += readNow;  \r\n    if(strstr(g_szRecvBuff, \"$$\\r\\n\\rpylon\"))  \r\n    {  \r\n     strcat(g_szRecvBuff, \"&gt;\"); \/\/readBytesUntil will skip this, so re-add  \r\n     foundTerminator = true;  \r\n     break; \/\/found end of the string  \r\n    }  \r\n    if(strstr(g_szRecvBuff, \"Press [Enter] to be continued,other key to exit\"))  \r\n    {  \r\n     \/\/we need to send new line character so battery continues the output  \r\n     Serial2.write(\"\\r\");  \r\n    }  \r\n    waitForSerial();  \r\n   }  \r\n  }  \r\n  if(recvBuffLen &gt; 0 )  \r\n  {  \r\n   if(foundTerminator == false)  \r\n   {  \r\n    Log(\"Failed to find pylon&gt; terminator\");  \r\n   }  \r\n  }  \r\n  return recvBuffLen;  \r\n }  \r\n bool readFromSerialAndSendResponse()  \r\n {  \r\n  const int recvBuffLen = readFromSerial();  \r\n  if(recvBuffLen &gt; 0)  \r\n  {  \r\n   server.sendContent(g_szRecvBuff);  \r\n   return true;  \r\n  }  \r\n  return false;  \r\n }  \r\n bool sendCommandAndReadSerialResponse(const char* pszCommand)  \r\n {  \r\n  switchBaud(115200);  \r\n  if(pszCommand[0] != '\\0')  \r\n  {  \r\n   Serial2.write(pszCommand);  \r\n  }  \r\n  Serial2.write(\"\\n\");  \r\n  const int recvBuffLen = readFromSerial();  \r\n  if(recvBuffLen &gt; 0)  \r\n  {  \r\n   return true;  \r\n  }  \r\n  \/\/wake up console and try again:  \r\n  wakeUpConsole();  \r\n  if(pszCommand[0] != '\\0')  \r\n  {  \r\n   Serial2.write(pszCommand);  \r\n  }  \r\n  Serial2.write(\"\\n\");  \r\n  return readFromSerial() &gt; 0;  \r\n }  \r\n void handleReq()  \r\n {  \r\n  #ifdef AUTHENTICATION  \r\n  if (!server.authenticate(www_username, www_password)) {  \r\n   return server.requestAuthentication();  \r\n  }  \r\n  #endif  \r\n  bool respOK;  \r\n  if(server.hasArg(\"code\") == false)  \r\n  {  \r\n   respOK = sendCommandAndReadSerialResponse(\"\");  \r\n  }  \r\n  else  \r\n  {  \r\n   respOK = sendCommandAndReadSerialResponse(server.arg(\"code\").c_str());  \r\n  }  \r\n  handleRoot();  \r\n }  \r\n void handleJsonOut()  \r\n {  \r\n  #ifdef AUTHENTICATION  \r\n  if (!server.authenticate(www_username, www_password)) {  \r\n   return server.requestAuthentication();  \r\n  }  \r\n  #endif  \r\n  if(sendCommandAndReadSerialResponse(\"pwr\") == false)  \r\n  {  \r\n   server.send(500, \"text\/plain\", \"Failed to get response to 'pwr' command\");  \r\n   return;  \r\n  }  \r\n  parsePwrResponse(g_szRecvBuff);  \r\n  prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));  \r\n  server.send(200, \"application\/json\", g_szRecvBuff);  \r\n }  \r\n void handleRoot() {  \r\n  #ifdef AUTHENTICATION  \r\n  if (!server.authenticate(www_username, www_password)) {  \r\n   return server.requestAuthentication();  \r\n  }  \r\n  #endif  \r\n  timeClient.update(); \/\/get ntp datetime  \r\n  unsigned long days = 0, hours = 0, minutes = 0;  \r\n  unsigned long val = os_getCurrentTimeSec();  \r\n  days = val \/ (3600*24);  \r\n  val -= days * (3600*24);  \r\n  hours = val \/ 3600;  \r\n  val -= hours * 3600;  \r\n  minutes = val \/ 60;  \r\n  val -= minutes*60;  \r\n  time_t epochTime = timeClient.getEpochTime();  \r\n  String formattedTime = timeClient.getFormattedTime();  \r\n  \/\/Get a time structure  \r\n  struct tm *ptm = gmtime ((time_t *)&amp;epochTime);   \r\n  int currentMonth = ptm-&gt;tm_mon+1;  \r\n  static char szTmp[9500] = \"\";   \r\n  long timezone= GMT \/ 3600;  \r\n  snprintf(szTmp, sizeof(szTmp)-1, \"&lt;html&gt;&lt;b&gt;Pylontech Battery LAN Interface&lt;\/b&gt;&lt;br&gt;Time GMT: %s (%s %d)&lt;br&gt;Uptime: %02d:%02d:%02d.%02d&lt;br&gt;&lt;br&gt;free heap: %u&lt;br&gt;MQTT-Server: %s&lt;br&gt;MQTT-root topic: %s\",   \r\n       formattedTime, \"GMT \", timezone,  \r\n       (int)days, (int)hours, (int)minutes, (int)val,   \r\n       ESP.getFreeHeap(),MQTT_SERVER,MQTT_TOPIC_ROOT);  \r\n  strncat(szTmp, \"&lt;BR&gt;&lt;a href='\/log'&gt;Runtime log&lt;\/a&gt;&lt;HR&gt;\", sizeof(szTmp)-1);  \r\n  strncat(szTmp, \"&lt;form action='\/req' method='get'&gt;Command:&lt;input type='text' name='code'\/&gt;&lt;input type='submit'&gt; &lt;a href='\/req?code=pwr'&gt;PWR&lt;\/a&gt; | &lt;a href='\/req?code=pwr%201'&gt;Power 1&lt;\/a&gt; | &lt;a href='\/req?code=pwr%202'&gt;Power 2&lt;\/a&gt; | &lt;a href='\/req?code=pwr%203'&gt;Power 3&lt;\/a&gt; | &lt;a href='\/req?code=pwr%204'&gt;Power 4&lt;\/a&gt; | &lt;a href='\/req?code=help'&gt;Help&lt;\/a&gt; | &lt;a href='\/req?code=log'&gt;Event Log&lt;\/a&gt; | &lt;a href='\/req?code=time'&gt;Time&lt;\/a&gt;&lt;br&gt;\", sizeof(szTmp)-1);  \r\n  \/\/strncat(szTmp, \"&lt;form action='\/req' method='get'&gt;Command:&lt;input type='text' name='code'\/&gt;&lt;input type='submit'&gt;&lt;a href='\/req?code=pwr'&gt;Power&lt;\/a&gt; | &lt;a href='\/req?code=help'&gt;Help&lt;\/a&gt; | &lt;a href='\/req?code=log'&gt;Event Log&lt;\/a&gt; | &lt;a href='\/req?code=time'&gt;Time&lt;\/a&gt;&lt;br&gt;\", sizeof(szTmp)-1);  \r\n  strncat(szTmp, \"&lt;textarea rows='80' cols='180'&gt;\", sizeof(szTmp)-1);  \r\n  \/\/strncat(szTmp, \"&lt;textarea rows='45' cols='180'&gt;\", sizeof(szTmp)-1);  \r\n  strncat(szTmp, g_szRecvBuff, sizeof(szTmp)-1);  \r\n  strncat(szTmp, \"&lt;\/textarea&gt;&lt;\/form&gt;\", sizeof(szTmp)-1);  \r\n  strncat(szTmp, \"&lt;\/html&gt;\", sizeof(szTmp)-1);  \r\n  \/\/send page  \r\n  server.send(200, \"text\/html\", szTmp);  \r\n }  \r\n unsigned long os_getCurrentTimeSec()  \r\n {  \r\n  static unsigned int wrapCnt = 0;  \r\n  static unsigned long lastVal = 0;  \r\n  unsigned long currentVal = millis();  \r\n  if(currentVal &lt; lastVal)  \r\n  {  \r\n   wrapCnt++;  \r\n  }  \r\n  lastVal = currentVal;  \r\n  unsigned long seconds = currentVal\/1000;  \r\n  \/\/millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter  \r\n  return (wrapCnt*4294967) + seconds;  \r\n }  \r\n void wakeUpConsole()  \r\n {  \r\n  switchBaud(1200);  \r\n  \/\/byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};  \r\n  \/\/Serial.write(wakeUpBuff, sizeof(wakeUpBuff));  \r\n  Serial.write(\"~20014682C0048520FCC3\\r\");  \r\n  delay(1000);  \r\n  byte newLineBuff[] = {0x0E, 0x0A};  \r\n  switchBaud(115200);  \r\n  for(int ix=0; ix&lt;10; ix++)  \r\n  {  \r\n   Serial.write(newLineBuff, sizeof(newLineBuff));  \r\n   delay(1000);  \r\n   if(Serial.available())  \r\n   {  \r\n    while(Serial.available())  \r\n    {  \r\n     Serial.read();  \r\n    }  \r\n    break;  \r\n   }  \r\n  }  \r\n }  \r\n #define MAX_PYLON_BATTERIES 8  \r\n struct pylonBattery  \r\n {  \r\n  bool isPresent;  \r\n  long soc;   \/\/Coulomb in %  \r\n  long voltage; \/\/in mW  \r\n  long current; \/\/in mA, negative value is discharge  \r\n  long tempr;  \/\/temp of case or BMS?  \r\n  long cellTempLow;  \r\n  long cellTempHigh;  \r\n  long cellVoltLow;  \r\n  long cellVoltHigh;  \r\n  char baseState[9];  \/\/Charge | Dischg | Idle  \r\n  char voltageState[9]; \/\/Normal  \r\n  char currentState[9]; \/\/Normal  \r\n  char tempState[9];  \/\/Normal  \r\n  char time[20];    \/\/2019-06-08 04:00:29  \r\n  char b_v_st[9];    \/\/Normal (battery voltage?)  \r\n  char b_t_st[9];    \/\/Normal (battery temperature?)  \r\n  bool isCharging()  const { return strcmp(baseState, \"Charge\")  == 0; }  \r\n  bool isDischarging() const { return strcmp(baseState, \"Dischg\")  == 0; }  \r\n  bool isIdle()    const { return strcmp(baseState, \"Idle\")   == 0; }  \r\n  bool isBalancing()  const { return strcmp(baseState, \"Balance\") == 0; }  \r\n  bool isNormal() const  \r\n  {  \r\n   if(isCharging()  == false &amp;&amp;  \r\n     isDischarging() == false &amp;&amp;  \r\n     isIdle()    == false &amp;&amp;  \r\n     isBalancing()  == false)  \r\n   {  \r\n    return false; \/\/base state looks wrong!  \r\n   }  \r\n   return strcmp(voltageState, \"Normal\") == 0 &amp;&amp;  \r\n       strcmp(currentState, \"Normal\") == 0 &amp;&amp;  \r\n       strcmp(tempState,  \"Normal\") == 0 &amp;&amp;  \r\n       strcmp(b_v_st,    \"Normal\") == 0 &amp;&amp;  \r\n       strcmp(b_t_st,    \"Normal\") == 0 ;  \r\n  }  \r\n };  \r\n struct batteryStack  \r\n {  \r\n  int batteryCount;  \r\n  int soc; \/\/in %, if charging: average SOC, otherwise: lowest SOC  \r\n  int temp; \/\/in mC, if highest temp is &gt; 15C, this will show the highest temp, otherwise the lowest  \r\n  long currentDC;  \/\/mAh current going in or out of the battery  \r\n  long avgVoltage;  \/\/in mV  \r\n  char baseState[9]; \/\/Charge | Dischg | Idle | Balance | Alarm!  \r\n  pylonBattery batts[MAX_PYLON_BATTERIES];  \r\n  bool isNormal() const  \r\n  {  \r\n   for(int ix=0; ix&lt;MAX_PYLON_BATTERIES; ix++)  \r\n   {  \r\n    if(batts[ix].isPresent &amp;&amp;   \r\n      batts[ix].isNormal() == false)  \r\n    {  \r\n     return false;  \r\n    }  \r\n   }  \r\n   return true;  \r\n  }  \r\n  \/\/in Wh  \r\n  long getPowerDC() const  \r\n  {  \r\n   return (long)(((double)currentDC\/1000.0)*((double)avgVoltage\/1000.0));  \r\n  }  \r\n  \/\/ power in Wh in charge  \r\n  float powerIN() const  \r\n  {  \r\n   if (currentDC &gt; 0) {  \r\n     return (float)(((double)currentDC\/1000.0)*((double)avgVoltage\/1000.0));  \r\n   } else {  \r\n     return (float)(0);  \r\n   }  \r\n  }  \r\n  \/\/ power in Wh in discharge  \r\n  float powerOUT() const  \r\n  {  \r\n   if (currentDC &lt; 0) {  \r\n     return (float)(((double)currentDC\/1000.0)*((double)avgVoltage\/1000.0)*-1);  \r\n   } else {  \r\n     return (float)(0);  \r\n   }  \r\n  }  \r\n  \/\/Wh estimated current on AC side (taking into account Sofar ME3000SP losses)  \r\n  long getEstPowerAc() const  \r\n  {  \r\n   double powerDC = (double)getPowerDC();  \r\n   if(powerDC == 0)  \r\n   {  \r\n    return 0;  \r\n   }  \r\n   else if(powerDC &lt; 0)  \r\n   {  \r\n    \/\/we are discharging, on AC side we will see less power due to losses  \r\n    if(powerDC &lt; -1000)  \r\n    {  \r\n     return (long)(powerDC*0.94);  \r\n    }  \r\n    else if(powerDC &lt; -600)  \r\n    {  \r\n     return (long)(powerDC*0.90);  \r\n    }  \r\n    else  \r\n    {  \r\n     return (long)(powerDC*0.87);  \r\n    }  \r\n   }  \r\n   else  \r\n   {  \r\n    \/\/we are charging, on AC side we will have more power due to losses  \r\n    if(powerDC &gt; 1000)  \r\n    {  \r\n     return (long)(powerDC*1.06);  \r\n    }  \r\n    else if(powerDC &gt; 600)  \r\n    {  \r\n     return (long)(powerDC*1.1);  \r\n    }  \r\n    else  \r\n    {  \r\n     return (long)(powerDC*1.13);  \r\n    }  \r\n   }  \r\n  }  \r\n };  \r\n batteryStack g_stack;  \r\n long extractInt(const char* pStr, int pos)  \r\n {  \r\n  return atol(pStr+pos);  \r\n }  \r\n void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)  \r\n {  \r\n  strOut[strOutSize-1] = '\\0';  \r\n  strncpy(strOut, pStr+pos, strOutSize-1);  \r\n  strOutSize--;  \r\n  \/\/trim right  \r\n  while(strOutSize &gt; 0)  \r\n  {  \r\n   if(isspace(strOut[strOutSize-1]))  \r\n   {  \r\n    strOut[strOutSize-1] = '\\0';  \r\n   }  \r\n   else  \r\n   {  \r\n    break;  \r\n   }  \r\n   strOutSize--;  \r\n  }  \r\n }  \r\n \/* Output has mixed \\r and \\r\\n  \r\n pwr  \r\n @  \r\n Power Volt  Curr  Tempr Tlow  Thigh Vlow  Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time         B.V.St  B.T.St   \r\n 1   49735 -1440 22000 19000 19000 3315  3317  Dischg  Normal  Normal  Normal  93%   2019-06-08 04:00:30 Normal  Normal   \r\n ....    \r\n 8   -   -   -   -   -   -   -   Absent  -    -    -    -    -          -    -      \r\n Command completed successfully  \r\n $$  \r\n pylon  \r\n *\/  \r\n bool parsePwrResponse(const char* pStr)  \r\n {  \r\n  if(strstr(pStr, \"Command completed successfully\") == NULL)  \r\n  {  \r\n   return false;  \r\n  }  \r\n  int chargeCnt  = 0;  \r\n  int dischargeCnt = 0;  \r\n  int idleCnt   = 0;  \r\n  int alarmCnt   = 0;  \r\n  int socAvg    = 0;  \r\n  int socLow    = 0;  \r\n  int tempHigh   = 0;  \r\n  int tempLow   = 0;  \r\n  memset(&amp;g_stack, 0, sizeof(g_stack));  \r\n  for(int ix=0; ix&lt;MAX_PYLON_BATTERIES; ix++)  \r\n  {  \r\n   char szToFind[32] = \"\";  \r\n   snprintf(szToFind, sizeof(szToFind)-1, \"\\r\\r\\n%d   \", ix+1);  \r\n   const char* pLineStart = strstr(pStr, szToFind);  \r\n   if(pLineStart == NULL)  \r\n   {  \r\n    return false;  \r\n   }  \r\n   pLineStart += 3; \/\/move past \\r\\r\\n  \r\n   extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));  \r\n   if(strcmp(g_stack.batts[ix].baseState, \"Absent\") == 0)  \r\n   {  \r\n    g_stack.batts[ix].isPresent = false;  \r\n   }  \r\n   else  \r\n   {  \r\n    g_stack.batts[ix].isPresent = true;  \r\n    extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));  \r\n    extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));  \r\n    extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));  \r\n    extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));  \r\n    extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));  \r\n    extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));  \r\n    g_stack.batts[ix].voltage = extractInt(pLineStart, 6);  \r\n    g_stack.batts[ix].current = extractInt(pLineStart, 13);  \r\n    g_stack.batts[ix].tempr  = extractInt(pLineStart, 20);  \r\n    g_stack.batts[ix].cellTempLow  = extractInt(pLineStart, 27);  \r\n    g_stack.batts[ix].cellTempHigh  = extractInt(pLineStart, 34);  \r\n    g_stack.batts[ix].cellVoltLow  = extractInt(pLineStart, 41);  \r\n    g_stack.batts[ix].cellVoltHigh  = extractInt(pLineStart, 48);  \r\n    g_stack.batts[ix].soc      = extractInt(pLineStart, 91);  \r\n    \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/ Post-process \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/  \r\n    g_stack.batteryCount++;  \r\n    g_stack.currentDC += g_stack.batts[ix].current;  \r\n    g_stack.avgVoltage += g_stack.batts[ix].voltage;  \r\n    socAvg += g_stack.batts[ix].soc;  \r\n    if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }  \r\n    else if(g_stack.batts[ix].isCharging()){chargeCnt++;}  \r\n    else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}  \r\n    else if(g_stack.batts[ix].isIdle()){idleCnt++;}  \r\n    else{ alarmCnt++; } \/\/should not really happen!  \r\n    if(g_stack.batteryCount == 1)  \r\n    {  \r\n     socLow = g_stack.batts[ix].soc;  \r\n     tempLow = g_stack.batts[ix].cellTempLow;  \r\n     tempHigh = g_stack.batts[ix].cellTempHigh;  \r\n    }  \r\n    else  \r\n    {  \r\n     if(socLow &gt; g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}  \r\n     if(tempHigh &lt; g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}  \r\n     if(tempLow &gt; g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}  \r\n    }  \r\n   }  \r\n  }  \r\n  \/\/now update stack state:  \r\n  g_stack.avgVoltage \/= g_stack.batteryCount;  \r\n  g_stack.soc = socLow;  \r\n  if(tempHigh &gt; 15000) \/\/15C  \r\n  {  \r\n   g_stack.temp = tempHigh; \/\/in the summer we highlight the warmest cell  \r\n  }  \r\n  else  \r\n  {  \r\n   g_stack.temp = tempLow; \/\/in the winter we focus on coldest cell  \r\n  }  \r\n  if(alarmCnt &gt; 0)  \r\n  {  \r\n   strcpy(g_stack.baseState, \"Alarm!\");  \r\n  }  \r\n  else if(chargeCnt == g_stack.batteryCount)  \r\n  {  \r\n   strcpy(g_stack.baseState, \"Charge\");  \r\n   g_stack.soc = (int)(socAvg \/ g_stack.batteryCount);  \r\n  }  \r\n  else if(dischargeCnt == g_stack.batteryCount)  \r\n  {  \r\n   strcpy(g_stack.baseState, \"Dischg\");  \r\n  }  \r\n  else if(idleCnt == g_stack.batteryCount)  \r\n  {  \r\n   strcpy(g_stack.baseState, \"Idle\");  \r\n  }  \r\n  else  \r\n  {  \r\n   strcpy(g_stack.baseState, \"Balance\");  \r\n  }  \r\n  return true;  \r\n }  \r\n void prepareJsonOutput(char* pBuff, int buffSize)  \r\n {  \r\n  memset(pBuff, 0, buffSize);  \r\n  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,   \r\n                                                                                                       g_stack.temp,   \r\n                                                                                                       g_stack.currentDC,   \r\n                                                                                                       g_stack.avgVoltage,   \r\n                                                                                                       g_stack.baseState,   \r\n                                                                                                       g_stack.batteryCount,   \r\n                                                                                                       g_stack.getPowerDC(),   \r\n                                                                                                       g_stack.getEstPowerAc(),  \r\n                                                                                                       g_stack.isNormal() ? \"true\" : \"false\");  \r\n }  \r\n void loop() {  \r\n #ifdef ENABLE_MQTT  \r\n  mqttLoop();  \r\n #endif  \r\n  ArduinoOTA.handle();  \r\n  server.handleClient();  \r\n  \/\/if there are bytes availbe on serial here - it's unexpected  \r\n  \/\/when we send a command to battery, we read whole response  \r\n  \/\/if we get anything here anyways - we will log it  \r\n  int bytesAv = Serial.available();  \r\n  if(bytesAv &gt; 0)  \r\n  {  \r\n   if(bytesAv &gt; 63)  \r\n   {  \r\n    bytesAv = 63;  \r\n   }  \r\n   char buff[64+4] = \"RCV:\";  \r\n   if(Serial.readBytes(buff+4, bytesAv) &gt; 0)  \r\n   {  \r\n    \/\/digitalWrite(LED, LOW);  \r\n    digitalWrite(LED, HIGH);  \r\n    delay(5);  \r\n    \/\/digitalWrite(LED, HIGH);\/\/high is off  \r\n    digitalWrite(LED, LOW);  \r\n    Log(buff);  \r\n   }  \r\n  }  \r\n }  \r\n #ifdef ENABLE_MQTT  \r\n #define ABS_DIFF(a, b) (a &gt; b ? a-b : b-a)  \r\n void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)  \r\n {  \r\n  char szTmp[16] = \"\";  \r\n  snprintf(szTmp, 15, \"%.2f\", newValue);  \r\n  if(force || ABS_DIFF(newValue, oldValue) &gt; minDiff)  \r\n  {  \r\n   mqttClient.publish(topic, szTmp, false);  \r\n  }  \r\n }  \r\n void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)  \r\n {  \r\n  char szTmp[16] = \"\";  \r\n  snprintf(szTmp, 15, \"%d\", newValue);  \r\n  if(force || ABS_DIFF(newValue, oldValue) &gt; minDiff)  \r\n  {  \r\n   mqttClient.publish(topic, szTmp, false);  \r\n  }  \r\n }  \r\n void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)  \r\n {  \r\n  if(force || strcmp(newValue, oldValue) != 0)  \r\n  {  \r\n   mqttClient.publish(topic, newValue, false);  \r\n  }  \r\n }  \r\n void pushBatteryDataToMqtt(const batteryStack&amp; lastSentData, bool forceUpdate \/* if true - we will send all data regardless if it's the same *\/)  \r\n {  \r\n  mqtt_publish_f(MQTT_TOPIC_ROOT \"soc\",     g_stack.soc,        lastSentData.soc,        0, forceUpdate);  \r\n  mqtt_publish_f(MQTT_TOPIC_ROOT \"temp\",     (float)g_stack.temp\/1000.0, (float)lastSentData.temp\/1000.0, 0.1, forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"currentDC\",  g_stack.currentDC,     lastSentData.currentDC,     1, forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"estPowerAC\",  g_stack.getEstPowerAc(),  lastSentData.getEstPowerAc(),  10, forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"battery_count\",g_stack.batteryCount,    lastSentData.batteryCount,    0, forceUpdate);  \r\n  mqtt_publish_s(MQTT_TOPIC_ROOT \"base_state\",  g_stack.baseState,     lastSentData.baseState      , forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"is_normal\",  g_stack.isNormal() ? 1:0,  lastSentData.isNormal() ? 1:0,  0, forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"getPowerDC\",  g_stack.getPowerDC(),    lastSentData.getPowerDC(),    1, forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"powerIN\",   g_stack.powerIN(),     lastSentData.powerIN(),     1, forceUpdate);  \r\n  mqtt_publish_i(MQTT_TOPIC_ROOT \"powerOUT\",   g_stack.powerOUT(),     lastSentData.powerOUT(),     1, forceUpdate);  \r\n  \/\/ publishing details  \r\n  for (int ix = 0; ix &lt; g_stack.batteryCount; ix++) {  \r\n   char ixBuff[50];  \r\n   String ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/voltage\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_f(ixBuff, g_stack.batts[ix].voltage \/ 1000.0, lastSentData.batts[ix].voltage \/ 1000.0, 0, forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/current\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_f(ixBuff, g_stack.batts[ix].current \/ 1000.0, lastSentData.batts[ix].current \/ 1000.0, 0, forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/soc\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_i(ixBuff, g_stack.batts[ix].soc, lastSentData.batts[ix].soc, 0, forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/charging\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_i(ixBuff, g_stack.batts[ix].isCharging()?1:0, lastSentData.batts[ix].isCharging()?1:0, 0, forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/discharging\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_i(ixBuff, g_stack.batts[ix].isDischarging()?1:0, lastSentData.batts[ix].isDischarging()?1:0, 0, forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/idle\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_i(ixBuff, g_stack.batts[ix].isIdle()?1:0, lastSentData.batts[ix].isIdle()?1:0, 0, forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/state\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_s(ixBuff, g_stack.batts[ix].isIdle()?\"Idle\":g_stack.batts[ix].isCharging()?\"Charging\":g_stack.batts[ix].isDischarging()?\"Discharging\":\"\", lastSentData.batts[ix].isIdle()?\"Idle\":lastSentData.batts[ix].isCharging()?\"Charging\":lastSentData.batts[ix].isDischarging()?\"Discharging\":\"\", forceUpdate);  \r\n   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + \"\/temp\";  \r\n   ixBattStr.toCharArray(ixBuff, 50);  \r\n   mqtt_publish_f(ixBuff, (float)g_stack.batts[ix].tempr\/1000.0, (float)lastSentData.batts[ix].tempr\/1000.0, 0.1, forceUpdate);  \r\n  }  \r\n }   \r\n void mqttLoop()  \r\n {  \r\n  \/\/if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)  \r\n  static unsigned long g_lastConnectionAttempt = 0;  \r\n  \/\/first: let's make sure we are connected to mqtt  \r\n  const char* topicLastWill = MQTT_TOPIC_ROOT \"availability\";  \r\n  if (!mqttClient.connected() &amp;&amp; (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt &gt; 60)) {  \r\n   if(mqttClient.connect(HOSTNAME, MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, \"offline\"))  \r\n   {  \r\n    Log(\"Connected to MQTT server: \" MQTT_SERVER);  \r\n    mqttClient.publish(topicLastWill, \"online\", true);  \r\n   }  \r\n   else  \r\n   {  \r\n    Log(\"Failed to connect to MQTT server.\");  \r\n   }  \r\n   g_lastConnectionAttempt = os_getCurrentTimeSec();  \r\n  }  \r\n  \/\/next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)  \r\n  static unsigned long g_lastDataSent = 0;  \r\n  if(mqttClient.connected() &amp;&amp;   \r\n    os_getCurrentTimeSec() - g_lastDataSent &gt; MQTT_PUSH_FREQ_SEC &amp;&amp;  \r\n    sendCommandAndReadSerialResponse(\"pwr\") == true)  \r\n  {  \r\n   static batteryStack lastSentData; \/\/this is the last state we sent to MQTT, used to prevent sending the same data over and over again  \r\n   static unsigned int callCnt = 0;  \r\n   parsePwrResponse(g_szRecvBuff);  \r\n   bool forceUpdate = (callCnt % 20 == 0); \/\/push all the data every 20th call  \r\n   pushBatteryDataToMqtt(lastSentData, forceUpdate);  \r\n   callCnt++;  \r\n   g_lastDataSent = os_getCurrentTimeSec();  \r\n   memcpy(&amp;lastSentData, &amp;g_stack, sizeof(batteryStack));  \r\n  }  \r\n  mqttClient.loop();  \r\n }  \r\n #endif \/\/ENABLE_MQTT  \r\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_8337\" class=\"pvc_stats all  \" data-element-id=\"8337\" style=\"\"><i class=\"pvc-stats-icon medium\" aria-hidden=\"true\"><svg aria-hidden=\"true\" focusable=\"false\" data-prefix=\"far\" data-icon=\"chart-bar\" role=\"img\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 512 512\" class=\"svg-inline--fa fa-chart-bar fa-w-16 fa-2x\"><path fill=\"currentColor\" d=\"M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z\" class=\"\"><\/path><\/svg><\/i> <img decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif\" border=0 \/><\/p>\n<div class=\"pvc_clear\"><\/div>\n<p>&nbsp; Ein KI-generierter Podcastbeitrag zum Bloginhalt&#8230; In dem Beitrag Pylontech PV-Akkustatus im HomeAssistant hatte ich das Projekt &#8222;Pylontech-Battery-Monitoring&#8220; der folgenden GitHub Links etwas aufgeh\u00fcbscht und eine Platine gezeichnet, um das ganze Konstrukt etwas kompakter und professioneller aufzubereiten.&nbsp; https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring https:\/\/github.com\/hidaba\/PylontechMonitoring Im Homeassistant wurden damit, bzw. werden, s\u00e4mtliche Batteriedaten der Pylontech Akku Module angezeigt. Super! Wenn ich&hellip; <br \/> <a class=\"read-more\" href=\"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/2025\/02\/10\/lan-fuer-pylontech-pv-akkustatus-opendtu-und-mehr-im-homeassistant\/\">Weiterlesen<\/a><\/p>\n","protected":false},"author":86,"featured_media":8354,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"ngg_post_thumbnail":0,"footnotes":""},"categories":[1,57],"tags":[2510,2609,2611,2509,2404,2515,2516,2517,2518,2612,2506,2504,2505,2610],"class_list":["post-8337","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-allgemeines","category-elektronikbastler","tag-akkustatus-im-homeassistant","tag-console2mqtt","tag-esp32_pylontech","tag-homeassistant","tag-mqtt","tag-pylontech","tag-pylontech-console","tag-pylontech-serial","tag-pylontech2mqtt","tag-universallan-interface","tag-us2000","tag-us3000","tag-us3000c","tag-wt32-eth01"],"a3_pvc":{"activated":true,"total_views":609,"today_views":0},"_links":{"self":[{"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/posts\/8337","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/users\/86"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/comments?post=8337"}],"version-history":[{"count":0,"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/posts\/8337\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/media\/8354"}],"wp:attachment":[{"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/media?parent=8337"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/categories?post=8337"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.fh-kaernten.at\/ingmarsretro\/wp-json\/wp\/v2\/tags?post=8337"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}