{"id":2546,"date":"2024-01-06T15:50:54","date_gmt":"2024-01-06T14:50:54","guid":{"rendered":"https:\/\/www.rustimation.eu\/?p=2546"},"modified":"2025-10-10T12:04:52","modified_gmt":"2025-10-10T10:04:52","slug":"solar-pylontech-akku-ueber-konsole-aufwecken","status":"publish","type":"post","link":"https:\/\/www.rustimation.eu\/index.php\/solar-pylontech-akku-ueber-konsole-aufwecken\/","title":{"rendered":"Solar &#8211; Pylontech Akku (C-Serie) \u00fcber Konsole aufwecken"},"content":{"rendered":"<p>Inzwischen hat mich auch das Solarfieber gepackt. In loser Folge werde ich hier meine Anlage beschreiben und Einzelaspekte herauspicken.<\/p>\n<p><span style=\"color: #ff0000;\"><strong>Update<\/strong><\/span>:\u00a0 Ein neuerer 4.8kWh Pylontech Akku namens US5000, hat eine andere Datenstruktur. Es kann sein, dass sp\u00e4testens ab Soft Version 2.0 alle Pylontech Akkus diese ge\u00e4nderte Struktur haben oder auch nur der US5000. Ich wei\u00df es mangels Vergleichsm\u00f6glichkeiten nicht genau. Bitte Input, wenn hier jemand etwas derartiges feststellt.<\/p>\n<pre class=\"nums:false lang:default decode:true\">Main Soft version   : B69.25.0.0\r\nSoft  version       : V2.0\r\nBoot  version       : V1.0\r\nComm version        : V2.0\r\nRelease Date        : 24-05-13<\/pre>\n<p>Die neue Datenstruktur in der Konsole f\u00fchrt dazu, dass der JSON und MQTT Output des PylontechMonitoring f\u00e4lschlicherweise Null Volt und \"Alarm!\" anzeigt. L\u00f6sung: siehe <a href=\"https:\/\/www.rustimation.eu\/index.php\/solar-pylontech-akku-ueber-konsole-aufwecken\/#Sketch_fuer_neue_Pylontech_Firmware\">neue Firmware<\/a> weiter unten. <span style=\"color: #ff0000;\"><strong>Ende<\/strong> <strong>Update<\/strong><\/span>.<\/p>\n<h2>Wohin mit dem \u00dcberschuss?<\/h2>\n<p>Mein \"Guerilla-Kraftwerk\" erzeugt theoretisch 1760Wp. Aufgrund der suboptimalen Ausrichtung nach WNW waren es im vergangenen Sommer lediglich ca. 1300W Maximalleistung. Die Module liegen mit 12\u00b0 recht flach, was die schlechte Ausrichtung etwas kompensiert.<\/p>\n<p><!--more--><\/p>\n<p>Immerhin habe ich im ersten halben Jahr 710kW Strom erzeugt. Standort ist Norditalien.<\/p>\n<p>Wenn die Schwimmbadpumpe nicht an ist, entsteht ein ganz ordentlicher \u00dcberschuss, den ich f\u00fcr 0 ct\/kWh den Elektrizit\u00e4tswerken schenke. Begrenzt auf die in Italien erlaubten 800W.<\/p>\n<p>Also wohin also mit dem \u00dcberschuss? Zum Experimentieren &#8211; man willl ja nicht gleich in die Vollen gehen &#8211; habe ich mir f\u00fcr knapp 600\u20ac einen Pylontech US2000C Akku mit einer Nennkapazit\u00e4t von 2,4kWh und 48V besorgt um eine AC Ladel\u00f6sung zu konstruieren. Anders als bei DC Laden wird der Ladestrom nicht direkt aus den Modulen \u00fcber einen Laderegler in die Batterie gepumpt, sondern der bereits in Wechselstrom (AC) umgewandelte \u00dcberschuss zur\u00fcck in Gleichstrom umgewandelt und dann in die Batterie gespeist anstatt ins Netz. Das mehrfache Umwandeln ist energetisch nicht optimal aber bei einem \"Balkonkraftwerk\" sehr viel einfacher zu realisieren. Ebenso bei der Nachr\u00fcstung &#8211; es muss an den existierenden Komponenten nichts ge\u00e4ndert werden. Au\u00dferdem k\u00f6nnen die Batterien an einem beliebigen Ort im Haus oder der Wohnung untergebracht werden. Steckdose reicht.<\/p>\n<p>Anfang 2024, also im Winter passiert zwar in punkto \u00dcberschuss wenig, aber zum Basteln und Programmieren reicht's. Inspiriert hat mich ein <a href=\"https:\/\/www.youtube.com\/watch?v=aLo-WuPQVuc&amp;pp=ygUZb2ZmeXMgd2Vya3N0YXR0IHB2IGFubGFnZQ%3D%3D\" target=\"_blank\" rel=\"noopener\">YouTube Beitrag von Offys Werkstatt<\/a>.<\/p>\n<p>Geladen wird der Akku mit einem Meanwell NBP-750 Netzteil, gesteuert \u00fcber einen <a href=\"https:\/\/trucki.de\/t2mg\/\" target=\"_blank\" rel=\"noopener\">Trucki2Meanwell Gateway Stick<\/a>.<\/p>\n<p>Entladen wird der Akku, d.h. ins Hausnetz eingespeist wird mit einem \u00fcbrig gebliebenen &#8211; zur Batterieeinspeisung zweckentfremdeten &#8211; Hoymiles HM-600 Wechselrichter, dessen Daten \u00fcber eine openDTU ausgelesen und mit einer selbst entwickelten <a href=\"https:\/\/www.rustimation.eu\/index.php\/solar-einspeiseregelung-mit-node-red\/\">Node-Red Logik<\/a> steuerungstechnisch verarbeitet werden.<\/p>\n<p><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Setup-e1705571171755.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignleft wp-image-2630 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Setup-e1705571171755-300x272.jpg\" alt=\"\" width=\"300\" height=\"272\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Setup-e1705571171755-300x272.jpg 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Setup-e1705571171755-768x697.jpg 768w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Setup-e1705571171755.jpg 793w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a> <a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8559.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignright wp-image-2576 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8559-244x300.jpg\" alt=\"\" width=\"244\" height=\"300\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8559-244x300.jpg 244w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8559.jpg 700w\" sizes=\"auto, (max-width: 244px) 100vw, 244px\" \/><\/a><\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n<p>Den gesamten ziemlich eklektischen Setup werde ich in verschiedenen Beitr\u00e4gen noch beleuchten.<\/p>\n<h2>Pylontech US2000C<\/h2>\n<p>Auf das Selberbasteln eines Akkupacks hatte ich wenig Lust, zumal die Ersparnis nicht besonders gro\u00df gewesen w\u00e4re.<\/p>\n<p>Vorteile des Pylontech Teils sind die Erweiterbarkeit, ein vern\u00fcnftiges Batterie Management System mit Soft Start &#8211; schont den Wechselrichter &#8211; und die M\u00f6glichkeit den Akkuzustand \u00fcber eine einfache, kleine Logik auszulesen und zu manipulieren.<\/p>\n<p style=\"padding-left: 40px;\"><span style=\"color: #808080;\">Noch ein Hinweis: Die hier beschriebene L\u00f6sung geht wahrscheinlich nur mit den Pylontech \"C\" Modellen wie US2000C etc., da diese den Aufweck-Kontakt im 8 poligen RJ45 Console Port des Ger\u00e4tes haben. Die aktuelleren US5000 (da gibt's kein \"C\" Modell)\u00a0 haben auch den Aufweckkontakt.<br \/>\nDie \u00e4lteren Modelle ohne \"C\", also US2000, US3000, US2000B etc. haben einen vierpoligen RJ10\/RJ11 Console Port. Lt. Handbuch gibt es dort keinen Aufweck-Kontakt.<br \/>\n<\/span><\/p>\n<h3 dir=\"auto\" tabindex=\"-1\">Pylontech Battery Monitoring via WiFi<\/h3>\n<p><a href=\"https:\/\/github.com\/irekzielinski\">Ireneusz Zielinski<\/a> hat schon vor einiger Zeit ein<a href=\"https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\" target=\"_blank\" rel=\"noopener\"> Projekt auf Github<\/a> bereit gestellt, mit dem sich der Akku \u00fcber WiFi monitoren l\u00e4sst. Es besteht im Wesentlichen aus einem ESP8266 Microcontroller (Wemos D1 Mini), einem Max3232 Transceiver, zwei Kondensatoren, einem umgemodelten Netzwerk Kabel und einem USB Netzteil. Fertig! Alle wesentlichen Infos finden sich auf Github. Kostenpunkt 25 &#8211; 35\u20ac.<\/p>\n<p>Damit l\u00e4sst sich der Akkustatus interaktiv \u00fcber eine schlichte Weboberfl\u00e4che auslesen und auch in Grenzen manipulieren &#8211; f\u00fcr die Integration in IoT Geschichten gibt es optional MQTT oder eine kleine API die auf Wunsch die Statusinfo auch\u00a0 als JSON ausspuckt. Beides l\u00e4sst sich hervorragend in Node-Red, Home Assistant oder andere IoT Plattformen integrieren.<\/p>\n<p>Diese Geschichte entwickeln wir jetzt weiter:<\/p>\n<figure id=\"attachment_2551\" aria-describedby=\"caption-attachment-2551\" style=\"width: 225px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8625.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2551 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8625-225x300.jpg\" alt=\"\" width=\"225\" height=\"300\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8625-225x300.jpg 225w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8625.jpg 756w\" sizes=\"auto, (max-width: 225px) 100vw, 225px\" \/><\/a><figcaption id=\"caption-attachment-2551\" class=\"wp-caption-text\">Console Monitoring Schaltung mit D1 Mini und MAX3232 Transceiver Breakout Board aber noch ohne Piggyback im Schaltkasten verklebt.<\/figcaption><\/figure>\n<figure id=\"attachment_3380\" aria-describedby=\"caption-attachment-3380\" style=\"width: 225px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/PylontechConsoleDaughterboard.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-3380 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/PylontechConsoleDaughterboard-225x300.jpg\" alt=\"\" width=\"225\" height=\"300\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/PylontechConsoleDaughterboard-225x300.jpg 225w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/PylontechConsoleDaughterboard.jpg 756w\" sizes=\"auto, (max-width: 225px) 100vw, 225px\" \/><\/a><figcaption id=\"caption-attachment-3380\" class=\"wp-caption-text\">Piggyback gesteckt. Ganz sch\u00f6n eng hier&#8230; Anklicken zum Vergr\u00f6\u00dfern<\/figcaption><\/figure>\n<h3>Nachteil: Der Wechselrichter zieht immer Strom<\/h3>\n<p>Leider zieht der Hoymiles Wechselrichter im Standby (also wenn er gerade nicht wechselt und richtet) ca. 1,8W pro Stunde aus dem Akku. Der State of Charge (SoC) verliert daher t\u00e4glich ca. 1,5%. Das ist irgendwie bl\u00f6d.<\/p>\n<p>Ich k\u00f6nnte nat\u00fcrlich den Wechselrichter DC seitig mit einem Leistungsrelais komplett von der Batterie tennen. Da immerhin 12A Stromst\u00e4rke zu schalten sind und gr\u00f6\u00dferen Hardware Aufwand erfordert, habe ich davon Abstand genommen. Die Wechselrichter-schonende\u00a0 \"Soft Start\" Funktion, welche die Spannung beim Hochfahren nicht schlagartig sondern langsam ansteigen l\u00e4sst, f\u00e4llt beim Schalten \u00fcber Relais nat\u00fcrlich flach. Man kann das zwar wieder \u00fcber kaskadiert geschaltete Vorwiderst\u00e4nde l\u00f6sen aber so t\u00fcrmt sich Aufwand \u00fcber Aufwand.<\/p>\n<p>Einfacher und billiger (&lt;5\u20ac) geht es mit Bordmitteln \u00fcber die oben beschriebene Monitoring Konsole: Der Akku hat n\u00e4mlich eine Shutdown Funktion, die \u00fcber die Konsole ausgel\u00f6st werden kann. Zum Beispiel so:<\/p>\n<pre class=\"lang:default decode:true\">http:\/\/192.168.178.69\/req?code=shut<\/pre>\n<p>Nur, sobald der Akku im Standby ist, kann ich ihn nicht so einfach \u00fcber die Konsole aufwecken, weil die ja auch abgeschaltet ist.<\/p>\n<h3>Aufwecken \u00fcber Pin 4 und 5<\/h3>\n<p>In der Betriebsanleitung des Akkus steht aber sinngem\u00e4\u00df: Legt man an Pin 4 (Plus) und Pin 5 (Minus) des Console Ports f\u00fcr \u22650,5 Sekunden ein Signal von 5-12 Volt und 5-15 mA an, wird der Akku wieder aufgeweckt.<\/p>\n<p>Das k\u00f6nnen wir mit dem sowieso schon vorhandenen D1 Mini und etwas Hardware recht einfach realisieren.<\/p>\n<h3>Hardware Erweiterung mit Tochterplatine<\/h3>\n<p>Wenn ihr die Console Platine schon gebaut und &#8211; so wie ich &#8211; fest montiert (verklebt) habt, dann habt ihr den D1 Mini hoffentlich auf Stiftsockelleisten montiert. Dann k\u00f6nnen wir eine Piggyback- oder Tochterplatine f\u00fcr die Wakeup Schaltung bauen und dazwischen stecken. Die neue Platine hat f\u00fcr den D1 Stiftsockelleisten mit l\u00e4ngeren Beinchen (Stacking Header) damit man sie auf die Console Platine aufstecken kann. Der D1 Mini obendrauf und fertig.<\/p>\n<figure id=\"attachment_2587\" aria-describedby=\"caption-attachment-2587\" style=\"width: 300px\" class=\"wp-caption alignleft\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8638.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2587 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8638-300x264.jpg\" alt=\"\" width=\"300\" height=\"264\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8638-300x264.jpg 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8638.jpg 568w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption id=\"caption-attachment-2587\" class=\"wp-caption-text\">Tochterplatine mit Steckplatz f\u00fcr D1 Mini<\/figcaption><\/figure>\n<figure id=\"attachment_2589\" aria-describedby=\"caption-attachment-2589\" style=\"width: 234px\" class=\"wp-caption alignright\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8640.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2589 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8640-234x300.jpg\" alt=\"\" width=\"234\" height=\"300\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8640-234x300.jpg 234w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8640.jpg 526w\" sizes=\"auto, (max-width: 234px) 100vw, 234px\" \/><\/a><figcaption id=\"caption-attachment-2589\" class=\"wp-caption-text\">Kein Meisterwerk der L\u00f6tkunst aber es funktioniert!<\/figcaption><\/figure>\n<figure id=\"attachment_2590\" aria-describedby=\"caption-attachment-2590\" style=\"width: 300px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8643.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-2590 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8643-300x261.jpg\" alt=\"\" width=\"300\" height=\"261\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8643-300x261.jpg 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IMG_8643.jpg 617w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption id=\"caption-attachment-2590\" class=\"wp-caption-text\">Mit gestecktem D1 Mini<\/figcaption><\/figure>\n<p>Falls der D1 Mini fest verl\u00f6tet ist, m\u00fcsst ihr eben an die 5V, GND und D5 Pins Dr\u00e4hte l\u00f6ten und die Platine anderweitig unterbringen oder alles neu bauen.<\/p>\n<h4>Schaltplan zum Nachr\u00fcsten<\/h4>\n<p>Nachdem wir den Sketch angepasst haben (siehe weiter unten) k\u00f6nnen wir einen http Request an den Microcontroller schicken (z.B. http:\/\/192.168.178.69\/<strong>wakeup<\/strong>). Damit wird der Pin D5 f\u00fcr ein paar Sekunden auf High gesetzt. Die 3.3V reichen aber nicht, das Signal direkt einzuspeisen. Auch die max. 12mA Leistung der GPIO Pins ist eher grenzwertig. Deshalb wird das Ganze mit der 5V Versorgungsspannung gel\u00f6st, die \u00fcber den MOSFET als Schalter und den 220 Ohm Vorwiderstand an den Pylontech Akku weitergeleitet wird.<br \/>\nGeht Pin D5 auf High, schaltet der MOSFET durch und der Strom flie\u00dft wie gew\u00fcnscht. Nat\u00fcrlich m\u00fcsst ihr dazu beim zweckentfremdeten Netzwerkkabel zum Console Port noch zwei Leitungen (zu Pin 4 und 5) freilegen und anschlie\u00dfen.<\/p>\n<p><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2564 size-full\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode.png\" alt=\"\" width=\"700\" height=\"588\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode.png 700w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode-300x252.png 300w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/a><\/p>\n<p>Der Pulldown Widerstand R1 ist wichtig, damit der MOSFET sauber sperrt. Der Widerstand R2 dient dazu, den Strom zu Pin4 bzw. Pin5 in den definierten Grenzen zu halten.<\/p>\n<p style=\"padding-left: 40px;\"><span style=\"color: #808080;\">Auch hier f\u00fchren wieder viele Wege nach Rom: Anstatt des MOSFET k\u00f6nnte man auch einen Transistor, ein Relais oder einen Optokoppler verwenden. Ich habe das hergenommen, was ich in meiner Bastelkiste gefunden habe.<\/span><\/p>\n<h4>St\u00fcckliste<\/h4>\n<ul>\n<li>Den D1 Mini haben wir ja schon.<\/li>\n<li>1 Logic Level n-Kanal MOSFET z.B. IRLZ44N oder alternativ und evtl. besser IRF3708 (derzeit schlecht verf\u00fcgbar) oder einen anderen Logic Level n-Kanal MOSFET<\/li>\n<li>1 Pulldown Widerstand mit 47k Ohm<\/li>\n<li>1 Vorwiderstand mit 220 Ohm oder 320 Ohm<\/li>\n<li>Dazu 1 Paar Stacking Header &#8211; ist im D1 Pack enthalten, wenn ihr das noch nicht weggeworfen habt \ud83d\ude09<\/li>\n<li>1 l\u00f6tbare 2er Schraubklemme<\/li>\n<li>kleines St\u00fcck Punkt-\/Streifenraster Platine &#8211; am besten mit 3er Punktreihen &#8211; z.B. Rademacher 790<\/li>\n<\/ul>\n<h3>Software Erweiterung<\/h3>\n<p>Hier nun der Arduino Sketch. Die von mir eingef\u00fcgten Erweiterungen sind gelb hervorgehoben.<\/p>\n<h4>Sketch f\u00fcr \u00e4ltere Pylontech Firmware<\/h4>\n<p>Ich empfehle, versuchsweise diese Version zu installieren; wenn bei der Ausgabe von JSON oder MQTT Output Null Volt und \"Alarm!\" erscheint, bitte den Sketch f\u00fcr die <a href=\"https:\/\/www.rustimation.eu\/index.php\/solar-pylontech-akku-ueber-konsole-aufwecken\/#Sketch_fuer_neue_Pylontech_Firmware\">neuere Firmware<\/a> (vermutlich ab Soft Version V2.0) verwenden.<\/p>\n<pre class=\"height-set:true lang:c++ mark:1-9,21,61,81,267-275 decode:true\">\/* Original software by Ireneusz Zielinski https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\r\nAddition of Wakeup Command (\/wakeup) by Christoph Krzikalla (www.rustimation.eu) \r\nreqires extra hardware to send 5V Signal to Pylontech battery console pin 4 and 5. \r\nWill only work with \"C\" Models of Pylontech Battery (US2000C etc. and newer US5000).\r\nInstructions for this specific version @ https:\/\/www.rustimation.eu\/index.php\/solar-pylontech-akku-ueber-konsole-aufwecken\r\nIMPORTANT: Also follow the instructions on Ireneusz Github pages. \r\nLibraries can be downloaded @ https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\/tree\/master\/libraries \r\nMake sure to copy the content of that subdirectory to the libraries directory of your Arduino IDE.\r\nAlso, make sure that the IDEs debug setting ist set to OFF or None *\/\r\n\r\n#include &lt;ESP8266WiFi.h&gt;\r\n#include &lt;ESP8266mDNS.h&gt;\r\n#include &lt;ArduinoOTA.h&gt;\r\n#include &lt;ESP8266WebServer.h&gt;\r\n\/\/#include &lt;ESP8266WebServer.h&gt;\r\n#include &lt;SimpleTimer.h&gt;\r\n#include &lt;TimeLib.h&gt; \/\/https:\/\/github.com\/PaulStoffregen\/Time\r\n#include &lt;ntp_time.h&gt;\r\n#include &lt;circular_log.h&gt;\r\n\r\nint battPinout = 14; \/\/ Assign pin for wakeup Signal trigger i.e: D5 on D1Mini \r\n\r\n\/\/IMPORTANT: Specify your WIFI settings:\r\n#define WIFI_SSID \"YourWiFiNetwork SSID\"\r\n#define WIFI_PASS \"yourSuperSecretPreSharedKey\"\r\n\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\r\n#ifdef ENABLE_MQTT\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        \"yourMQTTServerNameOrIPAdress\"\r\n#define MQTT_PORT          1883\r\n#define MQTT_USER          \"user\"\r\n#define MQTT_PASSWORD      \"password\"\r\n#define MQTT_TOPIC_ROOT    \"battery\/pylontech\/\"  \/\/this is where mqtt data will be pushed\r\n#define MQTT_PUSH_FREQ_SEC 2  \/\/maximum mqtt update frequency in seconds\r\n\r\n#include &lt;PubSubClient.h&gt;\r\nWiFiClient espClient;\r\nPubSubClient mqttClient(espClient);\r\n#endif \/\/ENABLE_MQTT\r\n\r\nchar g_szRecvBuff[7000];\r\n\r\nESP8266WebServer server(80);\r\nSimpleTimer timer;\r\ncircular_log&lt;7000&gt; g_log;\r\nbool ntpTimeReceived = false;\r\nint g_baudRate = 0;\r\n\r\nvoid Log(const char* msg)\r\n{\r\n  g_log.Log(msg);\r\n}\r\n\r\nvoid setup() {\r\n  pinMode(LED_BUILTIN, OUTPUT); \r\n  digitalWrite(LED_BUILTIN, HIGH);\/\/high is off\r\n  pinMode(battPinout, OUTPUT);\r\n  \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(\"PylontechBattery\");\r\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\r\n\r\n  for(int ix=0; ix&lt;10; ix++)\r\n  {\r\n    if(WiFi.status() == WL_CONNECTED)\r\n    {\r\n      break;\r\n    }\r\n\r\n    delay(1000);\r\n  }\r\n\r\n  ArduinoOTA.setHostname(\"RusticoBattery\");\r\n  ArduinoOTA.begin();\r\n  server.on(\"\/wakeup\", handleBattWakeup);  \/\/&lt;-- added by CKr\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    ESP.restart();\r\n  });\r\n  \r\n  server.begin(); \r\n  \r\n  syncTime();\r\n\r\n#ifdef ENABLE_MQTT\r\n  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);\r\n#endif\r\n\r\n  Log(\"Boot event\");\r\n}\r\n\r\nvoid handleLog()\r\n{\r\n  server.send(200, \"text\/html\", g_log.c_str());\r\n}\r\n\r\nvoid switchBaud(int newRate)\r\n{\r\n  if(g_baudRate == newRate)\r\n  {\r\n    return;\r\n  }\r\n  \r\n  if(g_baudRate != 0)\r\n  {\r\n    Serial.flush();\r\n    delay(20);\r\n    Serial.end();\r\n    delay(20);\r\n  }\r\n\r\n  char szMsg[50];\r\n  snprintf(szMsg, sizeof(szMsg)-1, \"New baud: %d\", newRate);\r\n  Log(szMsg);\r\n  \r\n  Serial.begin(newRate);\r\n  g_baudRate = newRate;\r\n\r\n  delay(20);\r\n}\r\n\r\nvoid waitForSerial()\r\n{\r\n  for(int ix=0; ix&lt;150;ix++)\r\n  {\r\n    if(Serial.available()) break;\r\n    delay(10);\r\n  }\r\n}\r\n\r\nint readFromSerial()\r\n{\r\n  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));\r\n  int recvBuffLen = 0;\r\n  bool foundTerminator = true;\r\n  \r\n  waitForSerial();\r\n  \r\n  while(Serial.available())\r\n  {\r\n    char szResponse[256] = \"\";\r\n    const int readNow = Serial.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      \r\n      strcat(g_szRecvBuff, szResponse);\r\n      recvBuffLen += readNow;\r\n\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\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        Serial.write(\"\\r\");\r\n      }\r\n\r\n      waitForSerial();\r\n    }\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\r\n  return recvBuffLen;\r\n}\r\n\r\nbool 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\r\n  return false;\r\n}\r\n\r\nbool sendCommandAndReadSerialResponse(const char* pszCommand)\r\n{\r\n  switchBaud(115200);\r\n\r\n  if(pszCommand[0] != '\\0')\r\n  {\r\n    Serial.write(pszCommand);\r\n  }\r\n  Serial.write(\"\\n\");\r\n\r\n  const int recvBuffLen = readFromSerial();\r\n  if(recvBuffLen &gt; 0)\r\n  {\r\n    return true;\r\n  }\r\n\r\n  \/\/wake up console and try again:\r\n  wakeUpConsole();\r\n\r\n  if(pszCommand[0] != '\\0')\r\n  {\r\n    Serial.write(pszCommand);\r\n  }\r\n  Serial.write(\"\\n\");\r\n\r\n  return readFromSerial() &gt; 0;\r\n}\r\n\r\nvoid handleReq()\r\n{\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\r\n  if(respOK)\r\n  {\r\n    server.send(200, \"text\/plain\", g_szRecvBuff);\r\n  }\r\n  else\r\n  {\r\n    server.send(500, \"text\/plain\", \"????\");\r\n  }\r\n}\r\n\r\nvoid handleJsonOut()\r\n{\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\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\r\n\/\/ ++++++++++++++Addition by CKr\r\n\/\/Routine zum Einschalten der Batterie                        \r\nvoid handleBattWakeup() {\r\n  digitalWrite(battPinout, HIGH); \/\/ send wakeup signal\r\n  delay(5000); \/\/ wait for a second\r\n  digitalWrite(battPinout, LOW); \/\/ turn off wakeup signal\r\n  server.send(200, \"text\/html\", \"Wakeup signal sent...\");\r\n}\r\n\/\/++++++++++++++End Addition\r\nvoid handleRoot() {\r\n  unsigned long days = 0, hours = 0, minutes = 0;\r\n  unsigned long val = os_getCurrentTimeSec();\r\n  \r\n  days = val \/ (3600*24);\r\n  val -= days * (3600*24);\r\n  \r\n  hours = val \/ 3600;\r\n  val -= hours * 3600;\r\n  \r\n  minutes = val \/ 60;\r\n  val -= minutes*60;\r\n  \r\n  static char szTmp[2500] = \"\";  \r\n  snprintf(szTmp, sizeof(szTmp)-1, \"&lt;html&gt;&lt;b&gt;2.4kWh Pylontech US2000C&lt;\/b&gt;&lt;br&gt;Time GMT: %d\/%02d\/%02d %02d:%02d:%02d (%s)&lt;br&gt;Uptime: %02d:%02d:%02d.%02d&lt;br&gt;&lt;br&gt;free heap: %u&lt;br&gt;Wifi RSSI: %d&lt;BR&gt;Wifi SSID: %s\", \r\n            year(), month(), day(), hour(), minute(), second(), \"GMT\",\r\n            (int)days, (int)hours, (int)minutes, (int)val, \r\n            ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str());\r\n\r\n\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;\/form&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;\", sizeof(szTmp)-1);\r\n  strncat(szTmp, \"&lt;\/html&gt;\", sizeof(szTmp)-1);\r\n  \r\n  server.send(200, \"text\/html\", szTmp);\r\n}\r\n\r\nunsigned 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\r\n  if(currentVal &lt; lastVal)\r\n  {\r\n    wrapCnt++;\r\n  }\r\n\r\n  lastVal = currentVal;\r\n  unsigned long seconds = currentVal\/1000;\r\n  \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\r\nvoid syncTime()\r\n{\r\n  \/\/get time from NTP\r\n  time_t currentTimeGMT = getNtpTime();\r\n  if(currentTimeGMT)\r\n  {\r\n    ntpTimeReceived = true;\r\n    setTime(currentTimeGMT);\r\n  }  \r\n  else\r\n  {\r\n    timer.setTimeout(5000, syncTime); \/\/try again in 5 seconds\r\n  }\r\n}\r\n\r\nvoid wakeUpConsole()\r\n{\r\n  switchBaud(1200);\r\n\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\r\n  byte newLineBuff[] = {0x0E, 0x0A};\r\n  switchBaud(115200);\r\n  \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\r\n    if(Serial.available())\r\n    {\r\n      while(Serial.available())\r\n      {\r\n        Serial.read();\r\n      }\r\n      \r\n      break;\r\n    }\r\n  }\r\n}\r\n\r\n#define MAX_PYLON_BATTERIES 8\r\n\r\nstruct 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\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  \r\n\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\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\r\nstruct 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\r\n  pylonBattery batts[MAX_PYLON_BATTERIES];\r\n\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\r\n    return true;\r\n  }\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\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\r\nbatteryStack g_stack;\r\n\r\n\r\nlong extractInt(const char* pStr, int pos)\r\n{\r\n  return atol(pStr+pos);\r\n}\r\n\r\nvoid 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  \r\n  \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\r\n    strOutSize--;\r\n  }\r\n}\r\n\r\n\/* Output has mixed \\r and \\r\\n\r\npwr\r\n\r\n@\r\n\r\nPower 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\r\n1     49735  -1440  22000  19000  19000  3315   3317   Dischg   Normal   Normal   Normal   93%      2019-06-08 04:00:30  Normal   Normal  \r\n\r\n....   \r\n\r\n8     -      -      -      -      -      -      -      Absent   -        -        -        -        -                    -        -       \r\n\r\nCommand completed successfully\r\n\r\n$$\r\n\r\npylon\r\n*\/\r\nbool 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  \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\r\n  memset(&amp;g_stack, 0, sizeof(g_stack));\r\n\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\r\n    const char* pLineStart = strstr(pStr, szToFind);\r\n    if(pLineStart == NULL)\r\n    {\r\n      return false;\r\n    }\r\n\r\n    pLineStart += 3; \/\/move past \\r\\r\\n\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\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\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\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  }\r\n\r\n  \/\/now update stack state:\r\n  g_stack.avgVoltage \/= g_stack.batteryCount;\r\n  g_stack.soc = socLow;\r\n\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\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\r\n\r\n  return true;\r\n}\r\n\r\nvoid 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\r\nvoid loop() {\r\n#ifdef ENABLE_MQTT\r\n  mqttLoop();\r\n#endif\r\n  \r\n  ArduinoOTA.handle();\r\n  server.handleClient();\r\n  timer.run();\r\n\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    \r\n    char buff[64+4] = \"RCV:\";\r\n    if(Serial.readBytes(buff+4, bytesAv) &gt; 0)\r\n    {\r\n      digitalWrite(LED_BUILTIN, LOW);\r\n      delay(5);\r\n      digitalWrite(LED_BUILTIN, HIGH);\/\/high is off\r\n\r\n      Log(buff);\r\n    }\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\nvoid 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\r\nvoid 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\r\nvoid 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\r\nvoid 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, 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}\r\n\r\nvoid 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\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(\"RusticoBattery\", 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\r\n    g_lastConnectionAttempt = os_getCurrentTimeSec();\r\n  }\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    \r\n    parsePwrResponse(g_szRecvBuff);\r\n\r\n    bool forceUpdate = (callCnt % 20 == 0); \/\/push all the data every 20th call\r\n    pushBatteryDataToMqtt(lastSentData, forceUpdate);\r\n    \r\n    callCnt++;\r\n    g_lastDataSent = os_getCurrentTimeSec();\r\n    memcpy(&amp;lastSentData, &amp;g_stack, sizeof(batteryStack));\r\n  }\r\n  \r\n  mqttClient.loop();\r\n}\r\n\r\n#endif \/\/ENABLE_MQTT<\/pre>\n<h4>Sketch f\u00fcr neue Pylontech Firmware<\/h4>\n<p>Zumindest beim US5000 mit Firmware ab 24-05-13 (oder evtl auch etwas vorher) hat der Output des Pylontech internen <em>pwr<\/em> Befehls eine andere Struktur. M\u00f6glicherweise sind auch alle anderen Pylontech Akkus (US2000C, US30000C) davon betroffen:<\/p>\n<h5>Beispiel alte Struktur<\/h5>\n<figure id=\"attachment_3150\" aria-describedby=\"caption-attachment-3150\" style=\"width: 1397px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Pylon_pwrAlt.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-3150 size-full\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Pylon_pwrAlt.png\" alt=\"\" width=\"1397\" height=\"855\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Pylon_pwrAlt.png 1397w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Pylon_pwrAlt-300x184.png 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Pylon_pwrAlt-1024x627.png 1024w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/Pylon_pwrAlt-768x470.png 768w\" sizes=\"auto, (max-width: 1397px) 100vw, 1397px\" \/><\/a><figcaption id=\"caption-attachment-3150\" class=\"wp-caption-text\">Alte Struktur des pwr Outputs &#8211; zwei US2000C Akkus<\/figcaption><\/figure>\n<h5><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu.png\"><span style=\"color: #000000;\">Beispiel neue Struktur<\/span><br \/>\n<\/a><\/h5>\n<figure id=\"attachment_3151\" aria-describedby=\"caption-attachment-3151\" style=\"width: 1832px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-3151 size-full\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu.png\" alt=\"\" width=\"1832\" height=\"827\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu.png 1832w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu-300x135.png 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu-1024x462.png 1024w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu-768x347.png 768w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/pylon_pwrNeu-1536x693.png 1536w\" sizes=\"auto, (max-width: 1832px) 100vw, 1832px\" \/><\/a><figcaption id=\"caption-attachment-3151\" class=\"wp-caption-text\">Neue Struktur des pwr Outputs &#8211; ein US5000 Akku<\/figcaption><\/figure>\n<p>F\u00fcr den JSON bzw. MQTT Output des PylontechMonitoring werden die Parameter in der <em>pwr<\/em> Tabelle anhand ihrer Position in der jeweiligen Zeile geparsed. Zum Beispiel befindet sich der baseState (Charge\/Dischg\/Idle\/Balance) anstatt an Position 55 jetzt an Stelle 91. Das f\u00fchrt nat\u00fcrlich zu einer gr\u00f6\u00dferen Verwirrung mit dem Effekt, dass JSON und MQTT nicht funktionieren.<\/p>\n<pre class=\"height-set:true height:250 lang:c++ mark:1-10,22,82,268-276,378,566-589 decode:true\">\/* Original software by Ireneusz Zielinski https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\r\nAddition of Wakeup Command (\/wakeup) by Christoph Krzikalla (www.rustimation.eu) \r\nreqires extra hardware to send 5V Signal to Pylontech battery console pin 4 and 5. \r\nWill only work with \"C\" Models of Pylontech Battery (US2000C etc. and newer US5000).\r\nInstructions for this specific version @ https:\/\/www.rustimation.eu\/index.php\/solar-pylontech-akku-ueber-konsole-aufwecken\r\nIMPORTANT: Also follow the instructions on Ireneusz Github pages. \r\nLibraries can be downloaded @ https:\/\/github.com\/irekzielinski\/Pylontech-Battery-Monitoring\/tree\/master\/libraries \r\nMake sure to copy the content of that subdirectory to the libraries directory of your Arduino IDE.\r\nUPDATED VERSION to reflect new output structure of the built-in pwr response.\r\nAlso, make sure that the IDEs debug setting ist set to OFF or None*\/\r\n\r\n#include &lt;ESP8266WiFi.h&gt;\r\n#include &lt;ESP8266mDNS.h&gt;\r\n#include &lt;ArduinoOTA.h&gt;\r\n#include &lt;ESP8266WebServer.h&gt;\r\n\/\/#include &lt;ESP8266WebServer.h&gt;\r\n#include &lt;SimpleTimer.h&gt;\r\n#include &lt;TimeLib.h&gt; \/\/https:\/\/github.com\/PaulStoffregen\/Time\r\n#include &lt;ntp_time.h&gt;\r\n#include &lt;circular_log.h&gt;\r\n\r\nint battPinout = 14; \/\/ Assign pin for wakeup Signal trigger i.e: D5 on D1Mini \r\n\r\n\/\/IMPORTANT: Specify your WIFI settings:\r\n#define WIFI_SSID \"YourWiFiNetwork SSID\"\r\n#define WIFI_PASS \"yourSuperSecretPreSharedKey\"\r\n\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\r\n#ifdef ENABLE_MQTT\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        \"yourMQTTServerNameOrIPAdress\"\r\n#define MQTT_PORT          1883\r\n#define MQTT_USER          \"user\"\r\n#define MQTT_PASSWORD      \"password\"\r\n#define MQTT_TOPIC_ROOT    \"battery\/pylontech\/\"  \/\/this is where mqtt data will be pushed\r\n#define MQTT_PUSH_FREQ_SEC 2  \/\/maximum mqtt update frequency in seconds\r\n\r\n#include &lt;PubSubClient.h&gt;\r\nWiFiClient espClient;\r\nPubSubClient mqttClient(espClient);\r\n#endif \/\/ENABLE_MQTT\r\n\r\nchar g_szRecvBuff[7000];\r\n\r\nESP8266WebServer server(80);\r\nSimpleTimer timer;\r\ncircular_log&lt;7000&gt; g_log;\r\nbool ntpTimeReceived = false;\r\nint g_baudRate = 0;\r\n\r\nvoid Log(const char* msg)\r\n{\r\n  g_log.Log(msg);\r\n}\r\n\r\nvoid setup() {\r\n  pinMode(LED_BUILTIN, OUTPUT); \r\n  digitalWrite(LED_BUILTIN, HIGH);\/\/high is off\r\n  pinMode(battPinout, OUTPUT);\r\n  \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(\"PylontechBattery\");\r\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\r\n\r\n  for(int ix=0; ix&lt;10; ix++)\r\n  {\r\n    if(WiFi.status() == WL_CONNECTED)\r\n    {\r\n      break;\r\n    }\r\n\r\n    delay(1000);\r\n  }\r\n\r\n  ArduinoOTA.setHostname(\"RusticoBattery\");\r\n  ArduinoOTA.begin();\r\n  server.on(\"\/wakeup\", handleBattWakeup);  \/\/&lt;-- added by CKr\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    ESP.restart();\r\n  });\r\n  \r\n  server.begin(); \r\n  \r\n  syncTime();\r\n\r\n#ifdef ENABLE_MQTT\r\n  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);\r\n#endif\r\n\r\n  Log(\"Boot event\");\r\n}\r\n\r\nvoid handleLog()\r\n{\r\n  server.send(200, \"text\/html\", g_log.c_str());\r\n}\r\n\r\nvoid switchBaud(int newRate)\r\n{\r\n  if(g_baudRate == newRate)\r\n  {\r\n    return;\r\n  }\r\n  \r\n  if(g_baudRate != 0)\r\n  {\r\n    Serial.flush();\r\n    delay(20);\r\n    Serial.end();\r\n    delay(20);\r\n  }\r\n\r\n  char szMsg[50];\r\n  snprintf(szMsg, sizeof(szMsg)-1, \"New baud: %d\", newRate);\r\n  Log(szMsg);\r\n  \r\n  Serial.begin(newRate);\r\n  g_baudRate = newRate;\r\n\r\n  delay(20);\r\n}\r\n\r\nvoid waitForSerial()\r\n{\r\n  for(int ix=0; ix&lt;150;ix++)\r\n  {\r\n    if(Serial.available()) break;\r\n    delay(10);\r\n  }\r\n}\r\n\r\nint readFromSerial()\r\n{\r\n  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));\r\n  int recvBuffLen = 0;\r\n  bool foundTerminator = true;\r\n  \r\n  waitForSerial();\r\n  \r\n  while(Serial.available())\r\n  {\r\n    char szResponse[256] = \"\";\r\n    const int readNow = Serial.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      \r\n      strcat(g_szRecvBuff, szResponse);\r\n      recvBuffLen += readNow;\r\n\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\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        Serial.write(\"\\r\");\r\n      }\r\n\r\n      waitForSerial();\r\n    }\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\r\n  return recvBuffLen;\r\n}\r\n\r\nbool 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\r\n  return false;\r\n}\r\n\r\nbool sendCommandAndReadSerialResponse(const char* pszCommand)\r\n{\r\n  switchBaud(115200);\r\n\r\n  if(pszCommand[0] != '\\0')\r\n  {\r\n    Serial.write(pszCommand);\r\n  }\r\n  Serial.write(\"\\n\");\r\n\r\n  const int recvBuffLen = readFromSerial();\r\n  if(recvBuffLen &gt; 0)\r\n  {\r\n    return true;\r\n  }\r\n\r\n  \/\/wake up console and try again:\r\n  wakeUpConsole();\r\n\r\n  if(pszCommand[0] != '\\0')\r\n  {\r\n    Serial.write(pszCommand);\r\n  }\r\n  Serial.write(\"\\n\");\r\n\r\n  return readFromSerial() &gt; 0;\r\n}\r\n\r\nvoid handleReq()\r\n{\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\r\n  if(respOK)\r\n  {\r\n    server.send(200, \"text\/plain\", g_szRecvBuff);\r\n  }\r\n  else\r\n  {\r\n    server.send(500, \"text\/plain\", \"????\");\r\n  }\r\n}\r\n\r\nvoid handleJsonOut()\r\n{\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\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\r\n\/\/ ++++++++++++++Addition by CKr\r\n\/\/Routine zum Einschalten der Batterie                        \r\nvoid handleBattWakeup() {\r\n  digitalWrite(battPinout, HIGH); \/\/ send wakeup signal\r\n  delay(5000); \/\/ wait for a second\r\n  digitalWrite(battPinout, LOW); \/\/ turn off wakeup signal\r\n  server.send(200, \"text\/html\", \"Wakeup signal sent...\");\r\n}\r\n\/\/++++++++++++++End Addition\r\nvoid handleRoot() {\r\n  unsigned long days = 0, hours = 0, minutes = 0;\r\n  unsigned long val = os_getCurrentTimeSec();\r\n  \r\n  days = val \/ (3600*24);\r\n  val -= days * (3600*24);\r\n  \r\n  hours = val \/ 3600;\r\n  val -= hours * 3600;\r\n  \r\n  minutes = val \/ 60;\r\n  val -= minutes*60;\r\n  \r\n  static char szTmp[2500] = \"\";  \r\n  snprintf(szTmp, sizeof(szTmp)-1, \"&lt;html&gt;&lt;b&gt;2.4kWh Pylontech US2000C&lt;\/b&gt;&lt;br&gt;Time GMT: %d\/%02d\/%02d %02d:%02d:%02d (%s)&lt;br&gt;Uptime: %02d:%02d:%02d.%02d&lt;br&gt;&lt;br&gt;free heap: %u&lt;br&gt;Wifi RSSI: %d&lt;BR&gt;Wifi SSID: %s\", \r\n            year(), month(), day(), hour(), minute(), second(), \"GMT\",\r\n            (int)days, (int)hours, (int)minutes, (int)val, \r\n            ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str());\r\n\r\n\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;\/form&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;\", sizeof(szTmp)-1);\r\n  strncat(szTmp, \"&lt;\/html&gt;\", sizeof(szTmp)-1);\r\n  \r\n  server.send(200, \"text\/html\", szTmp);\r\n}\r\n\r\nunsigned 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\r\n  if(currentVal &lt; lastVal)\r\n  {\r\n    wrapCnt++;\r\n  }\r\n\r\n  lastVal = currentVal;\r\n  unsigned long seconds = currentVal\/1000;\r\n  \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\r\nvoid syncTime()\r\n{\r\n  \/\/get time from NTP\r\n  time_t currentTimeGMT = getNtpTime();\r\n  if(currentTimeGMT)\r\n  {\r\n    ntpTimeReceived = true;\r\n    setTime(currentTimeGMT);\r\n  }  \r\n  else\r\n  {\r\n    timer.setTimeout(5000, syncTime); \/\/try again in 5 seconds\r\n  }\r\n}\r\n\r\nvoid wakeUpConsole()\r\n{\r\n  switchBaud(1200);\r\n\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\r\n  byte newLineBuff[] = {0x0E, 0x0A};\r\n  switchBaud(115200);\r\n  \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\r\n    if(Serial.available())\r\n    {\r\n      while(Serial.available())\r\n      {\r\n        Serial.read();\r\n      }\r\n      \r\n      break;\r\n    }\r\n  }\r\n}\r\n\r\n#define MAX_PYLON_BATTERIES 8\r\n\r\nstruct 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 isAbsent[7]; \/\/ new variable for \"Absent\" Marker to stop iteration &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;\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\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  \r\n\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\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\r\nstruct 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\r\n  pylonBattery batts[MAX_PYLON_BATTERIES];\r\n\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\r\n    return true;\r\n  }\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\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\r\nbatteryStack g_stack;\r\n\r\n\r\nlong extractInt(const char* pStr, int pos)\r\n{\r\n  return atol(pStr+pos);\r\n}\r\n\r\nvoid 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  \r\n  \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\r\n    strOutSize--;\r\n  }\r\n}\r\n\r\n\/* Output has mixed \\r and \\r\\n\r\npwr\r\n\r\n@\r\n\r\nPower 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\r\n1     49735  -1440  22000  19000  19000  3315   3317   Dischg   Normal   Normal   Normal   93%      2019-06-08 04:00:30  Normal   Normal  \r\n\r\n....   \r\n\r\n8     -      -      -      -      -      -      -      Absent   -        -        -        -        -                    -        -       \r\n\r\nCommand completed successfully\r\n\r\n$$\r\n\r\npylon\r\n*\/\r\nbool 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  \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\r\n  memset(&amp;g_stack, 0, sizeof(g_stack));\r\n\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\r\n    const char* pLineStart = strstr(pStr, szToFind);\r\n    if(pLineStart == NULL)\r\n    {\r\n      return false;\r\n    }\r\n\r\n    pLineStart += 3; \/\/move past \\r\\r\\n\r\n\r\n    extractStr(pLineStart, 91, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));\/\/ ##########  new offset 91 instead of 50\r\n    extractStr(pLineStart, 55, g_stack.batts[ix].isAbsent, sizeof(g_stack.batts[ix].isAbsent));  \/\/ ########## new variable for check of \"Absent\" text #########\r\n\r\n    if(strcmp(g_stack.batts[ix].isAbsent, \"Absent\") == 0)                                        \/\/ ########## changed from baseState to isAbsent\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, 100, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState)); \/\/ ########## new offset\r\n      extractStr(pLineStart, 109, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState)); \/\/ ########## new offset\r\n      extractStr(pLineStart, 118, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));       \/\/ ########## new offset\r\n      extractStr(pLineStart, 136, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));                 \/\/ ########## new offset\r\n      extractStr(pLineStart, 157, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));             \/\/ ########## new offset\r\n      extractStr(pLineStart, 166, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));             \/\/ ########## new offset\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, 43);                                       \/\/ ########## new offset\r\n      g_stack.batts[ix].cellVoltLow    = extractInt(pLineStart, 59);                                       \/\/ ########## new offset\r\n      g_stack.batts[ix].cellVoltHigh   = extractInt(pLineStart, 75);                                       \/\/ ########## new offset\r\n      g_stack.batts[ix].soc            = extractInt(pLineStart, 127);                                      \/\/ ########## new offset\r\n\r\n\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\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\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  }\r\n\r\n  \/\/now update stack state:\r\n  g_stack.avgVoltage \/= g_stack.batteryCount;\r\n  g_stack.soc = socLow;\r\n\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\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\r\n\r\n  return true;\r\n}\r\n\r\nvoid 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\r\nvoid loop() {\r\n#ifdef ENABLE_MQTT\r\n  mqttLoop();\r\n#endif\r\n  \r\n  ArduinoOTA.handle();\r\n  server.handleClient();\r\n  timer.run();\r\n\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    \r\n    char buff[64+4] = \"RCV:\";\r\n    if(Serial.readBytes(buff+4, bytesAv) &gt; 0)\r\n    {\r\n      digitalWrite(LED_BUILTIN, LOW);\r\n      delay(5);\r\n      digitalWrite(LED_BUILTIN, HIGH);\/\/high is off\r\n\r\n      Log(buff);\r\n    }\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\nvoid 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\r\nvoid 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\r\nvoid 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\r\nvoid 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, 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}\r\n\r\nvoid 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\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(\"RusticoBattery\", 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\r\n    g_lastConnectionAttempt = os_getCurrentTimeSec();\r\n  }\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    \r\n    parsePwrResponse(g_szRecvBuff);\r\n\r\n    bool forceUpdate = (callCnt % 20 == 0); \/\/push all the data every 20th call\r\n    pushBatteryDataToMqtt(lastSentData, forceUpdate);\r\n    \r\n    callCnt++;\r\n    g_lastDataSent = os_getCurrentTimeSec();\r\n    memcpy(&amp;lastSentData, &amp;g_stack, sizeof(batteryStack));\r\n  }\r\n  \r\n  mqttClient.loop();\r\n}\r\n\r\n#endif \/\/ENABLE_MQTT<\/pre>\n<h4>Vorsicht Falle: Serielle Schnittstelle<\/h4>\n<p>Der D1 Mini Microcontroller beherrscht leider keinen richtigen Debug Modus. Will man eine eigene Software bauen oder ein vorhandenes Programm weiterentwickeln, behilft man sich \u00fcblicherweise mit vielen <span class=\"lang:default decode:true crayon-inline \">Serial.println(\"text\");<\/span>\u00a0 Statements, die dann in der seriellen Konsole den Inhalt von Variablen wiedergeben oder anzeigen, dass das Programm gerade an eine bestimmten Stelle angekommen ist.<\/p>\n<p>Bei unserem Programm hier geht das leider nicht.\u00a0 Die <span class=\"lang:default decode:true crayon-inline\">Serial.println<\/span>\u00a0 Statements werden n\u00e4mlich auch an die angeschlossene Pylontech Konsole geschickt und bringen das Monitoring Programm geh\u00f6rig durcheinander. Nach meinem Verst\u00e4ndnis liegt das daran, dass die serielle USB Schnittstelle identisch zu den RX TX Pins auf der Platine ist. Was man eigentlich nur an die Konsole schicken wollte, wird sowohl an die Pins, als auch an die USB Buchse geschickt.<\/p>\n<p>Will man also das oben stehende Monitoring Programm von Ireneus mit anderen Funktionen, z.B. <a href=\"https:\/\/www.rustimation.eu\/index.php\/solar-trucki-2-huawei-gateway-im-einsatz\/#Display\" target=\"_blank\" rel=\"noopener\">einem Display erweitern<\/a>, sollte man den D1 Mini w\u00e4hrend der Entwicklung bzw.\u00a0 beim Debuggen von der Pylontech Batterie trennen.<\/p>\n<p><strong>Und nochwas<\/strong>: In letzter Zeit habe ich mit ESP32 Controllern herumexperimentiert, hier kann man den Debug Modus des Arduino IDE prima nutzen &#8211; im IDE stellt man unter <em>Werkzeuge<\/em> den Debugmodus auf eine der beiden Schnittstellen ein und etwas weiter unten, welche Daten \u00fcber den Debug Port ausgegeben werden sollen.<\/p>\n<p>Nach einem kleinen Update des Konsolensketches habe ich meinen D1 Mini\u00a0 mit aktiviertem Debug Modus neu programmiert und bin dann schier verzweifelt, weil nichts mehr funktioniert hat.\u00a0 Irgend wann bin ich darauf gekommen, was die Ursache war.<\/p>\n<p><strong>Problem<\/strong> <strong>ist<\/strong>, vergisst man, beim Programmieren des ESP8266 (D1 Mini) diesen Port wieder zu deaktivieren und die Ausgabe auf \"None\" zu stellen, kommt Quark heraus. Die ganzen Debug Meldungen wierden jetzt auch an die Batterie geschickt, was zu einer heftigen Verwirrung derselben f\u00fchrt. Sie versteht nur Bahnhof. Der D1 Mini hat zwar bei GPIO 13 (RXD) und 15 (TXD)\u00a0 einen zweiten seriellen Port, aber dieser wird in unserem Sketch nicht genutzt.<\/p>\n<p><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-3339 size-medium aligncenter\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE1-300x199.png\" alt=\"\" width=\"300\" height=\"199\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE1-300x199.png 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE1.png 647w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<figure id=\"attachment_3340\" aria-describedby=\"caption-attachment-3340\" style=\"width: 300px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-3340 size-medium\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE2-300x184.png\" alt=\"\" width=\"300\" height=\"184\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE2-300x184.png 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/IDE2.png 763w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><figcaption id=\"caption-attachment-3340\" class=\"wp-caption-text\">Anklicken zum Vergr\u00f6\u00dfern<\/figcaption><\/figure>\n<h3>Platine von Grund auf neu<\/h3>\n<p>Am besten w\u00e4re es nat\u00fcrlich, das alles zusammen mit der Console Monitoring Elektronik auf einer einzigen Lochraster-Platine aufzubauen. Hier der Komplett-Schaltplan f\u00fcr die Konsole und den Wakeup.<\/p>\n<h2><a href=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2565\" src=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode-1.png\" alt=\"\" width=\"1009\" height=\"649\" srcset=\"https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode-1.png 1009w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode-1-300x193.png 300w, https:\/\/www.rustimation.eu\/wordpress\/wp-content\/uploads\/2024\/01\/2024-01-06-18_28_36-EasyEDAStandard-6.5.39-Projects-Offline-mode-1-768x494.png 768w\" sizes=\"auto, (max-width: 1009px) 100vw, 1009px\" \/><\/a>Fazit<\/h2>\n<p>Die oben beschrieben L\u00f6sung habe ich jetzt seit \u00fcber einem Jahr (Stand M\u00e4rz 2025) im Echtbetrieb und muss sagen, es funktioniert einwandfrei.<\/p>\n<p>Nach dem Aufwecken des Akkus sollte man darauf achten, dass der Einspeise Wechselrichter nicht ungewollt von selbst lostickert, da der Hoymiles HM600 dann meint, es w\u00fcrde gerade die Sonne aufgehen. Nach einer kurzen Zeit von ca. 15 Sekunden den WR auf \"aus\" stellen, wenn man gerade nicht einspeisen will. Zum Beispiel weil gerade der Akku l\u00e4dt.<\/p>\n<p>All das habe ich auch ganz brav auf <a href=\"https:\/\/github.com\/Rustimation\/Pylontech-Battery-Monitoring\">Github<\/a> dokumentiert, enstprechend einen Fork der Original L\u00f6sung von Ireneusz gezogen.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Inzwischen hat mich auch das Solarfieber gepackt. In loser Folge werde ich hier meine Anlage beschreiben und Einzelaspekte herauspicken. Update:\u00a0 Ein neuerer 4.8kWh Pylontech Akku namens US5000, hat eine andere Datenstruktur. Es kann sein, dass sp\u00e4testens ab Soft Version 2.0 alle Pylontech Akkus diese ge\u00e4nderte Struktur haben oder auch nur der US5000. Ich wei\u00df es &hellip; <a href=\"https:\/\/www.rustimation.eu\/index.php\/solar-pylontech-akku-ueber-konsole-aufwecken\/\" class=\"more-link\"><span class=\"screen-reader-text\">Solar &#8211; Pylontech Akku (C-Serie) \u00fcber Konsole aufwecken<\/span> weiterlesen <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[112,102,53,151,181,69],"tags":[100],"class_list":["post-2546","post","type-post","status-publish","format-standard","hentry","category-d1-mini","category-esp8266-esp32","category-iot","category-node-red","category-solar","category-tips-tricks","tag-esp8266"],"_links":{"self":[{"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/posts\/2546","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/comments?post=2546"}],"version-history":[{"count":1,"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/posts\/2546\/revisions"}],"predecessor-version":[{"id":3690,"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/posts\/2546\/revisions\/3690"}],"wp:attachment":[{"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/media?parent=2546"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/categories?post=2546"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.rustimation.eu\/index.php\/wp-json\/wp\/v2\/tags?post=2546"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}