All posts by ingmarsretro

Repairing a TRADFRI smart plug – When the relay goes haywire

Loading

A quick note: Parts of the wording in this post were drafted with the help of AI. … (clearly recognizable in the style:

A repair story from the world of smart Swedish furniture.


The symptom: click-click-click…

As so often happens, it started with an annoying noise. My TRADFRI socket from the big Swedish furniture store had suddenly decided to put on a percussion performance. Click-click-click-click—the switching relay kept turning on and off as if it were having an epileptic seizure. The connected lamp flickered like in a horror movie, and I knew: I had to do something about this again.

The diagnosis: An old acquaintance

Anyone who has been repairing electronics for a long time is familiar with the problem: electrolytic capacitors are the Achilles heel of many devices. They dry out, lose capacity, and suddenly nothing works anymore. I have created a video of the symptoms of this problem:

The culprit: a 680µF/10V electrolytic capacitor in the power supply of the socket. When it stops working, the power supply becomes unstable and the control circuit panics. The relay switches back and forth uncontrollably—click, click, click.

Figure 1: Close-up of the defective electrolytic capacitor on the circuit board

The battle with the case: Deluxe gluing extravaganza

Now comes the part that turns a simple repair into a test of patience. IKEA apparently thought, “Anyone who wants to repair this TRADFRI socket should have to work for it!” The housing is not only secured with screws, but also glued. And really glued.

Step 1: The visible screws

First, the good news: there are indeed screws. After removing them, you briefly think, “Aha, I’ve got it!”—but no such luck.

Figure 2: Back side open with screws visible

Step 2: The invisible adhesive trap

The housing is rock solid. There is a continuous adhesive seam between the two halves of the housing that cannot be removed with bare hands. Here is my tool recommendation:

  • Several plastic spudgers or old EC cards
  • or a cable knife (if there is no other option)
  • A lot of patience
  • Even more patience

The technique: Heat the case all around (NOT too hot, otherwise the plastic tabs will melt!), then carefully insert the spudger between the two halves of the case. Work your way forward millimeter by millimeter. The whole thing will creak, groan, and squeak—but eventually the adhesive or plastic will give way.

Figure 3: Housing is open

Step 3: Don’t forget the clips

In addition to the adhesive, there are hidden locking tabs on the sides. These must be released at the same time while prying open the case. A third arm would be helpful here. However, a vise with soft jaws will also do the trick.

After about 15 minutes of patient work (and a few creative curses), the case finally comes apart. Fortunately, the breakage is manageable—with this type of bonding, the case is often just scrap metal afterwards.

The repair: Replace the capacitor

Now it’s easy. The defective electrolytic capacitor is quickly identified:

Required materials:

  • 1x electrolytic capacitor 680µF / 10V (105°C types recommended)
  • Soldering iron (20-60W)
  • Desoldering braid or desoldering pump
  • solder

The exchange:

  1. Desoldering the old capacitor: Heat the solder joints from the underside using the desoldering braid and remove the tin. Caution: The circuit board is double-sided, so remove the tin from both sides!
  2. Insert new capacitor: Pay attention to polarity! The negative pole is marked on the capacitor (usually a white stripe with a minus sign). On the circuit board, a plus sign is usually marked on the corresponding pad.
  3. Soldering: Insert the legs, solder from below, and cut off any excess wire.
Figure 4: New capacitor soldered onto the circuit board

The functional test

Carefully place the circuit board back into the housing (do not seal it yet!), plug it in, and… silence. Wonderful silence. No more clicking. The LED lights up steadily, the relay switches cleanly, and the outlet works perfectly again.

Das Gehäuse wieder schließen

Now comes the crucial question: How do I close this thing again?

The old adhesive is worn out. My solution: two-component epoxy resin. Apply a thin layer to the areas to be bonded, press the housing halves together, secure with adhesive tape, and leave to harden overnight. Alternatively: use small screws at strategic points (where there are no electronic components in the way).

Conclusion: Is it worth repairing?

Pro:

  • The socket is working again.
  • Cost: ~2 euros for the capacitor
  • Satisfying feeling of repair
  • One less device in the trash

Contra:

  • Time required: approx.  0.5 hours
  • Opening the housing is destructive
  • New TRADFRI socket costs only approx. 10-15 euros
  • The warranty is gone, of course.

For me personally, it was worth it. Not because of the cost savings, but on principle. This throwaway mentality is just annoying. A simple capacitor for €2 gets the device working again—I can’t just throw away the whole thing!

Tips for imitators

  1. Safety first: Before opening, unplug the device and wait 5 minutes!
  2. Document: Take photos at every step. Helps with assembly.
  3. Have spare parts ready: Order the capacitor before you open up.
  4. Housing replacement: Ask IKEA if replacement housings are available (spoiler: probably not ).

closing remarks

The TRADFRI socket is an affordable smart home device with a rather stupid design decision: the glued housing. If it were just screwed on, anyone could carry out this simple repair. This turns a 5-minute repair into half an hour of fiddling around.

But: It is doable. And it works. My TRADFRI socket has been running for weeks without any problems. The clicking is history.

With that in mind: repair your devices, folks! It’s worth it.


Have you ever repaired TRADFRI devices? Share your experiences in the comments!

Disclaimer: This repair is carried out at your own risk. Working with mains voltage poses a risk to life. If in doubt: keep your hands off and consult a specialist!

MiniArcade Machine—From Paper Model to Retro Arcade Machine

Loading

Some projects arise from a spontaneous idea, while others develop over months. Our mini arcade machine definitely belongs to the second category. It all started on a cold winter day when my son and I sat down at the kitchen table to start another little craft project. At the time, we had no idea that a simple paper model would eventually turn into a working mini arcade machine. But that’s exactly what happened—and in this post, I’d like to take you step by step through how we implemented this project.

The inspiration: a paper arcade machine
The idea to build a mini arcade machine came to us quite spontaneously. A few years ago, I built a tabletop or bartop arcade machine, which my son regularly monopolizes and plays with. At some point, he got a very small “mini arcade machine” (which you can buy in the Far East for less than 10 euros). However, due to its very small dimensions and screen size of just under 5 cm, it only provides short-lived enjoyment. The quality of the pre-installed games is also somewhat borderline. As he was playing with the little thing again, he asked, “Can we make something like this out of paper?” Of course we could! So we found an old shipping box that was left over from a delivery from a large online retailer and began to draw the first designs.

Step 1: Sketches and planning
Using a ruler and pencil, we drew the outline of the machine directly onto the cardboard. We based our design on the dimensions of the small arcade machine: a display tilted slightly backward, a wide base, and a front panel with space for buttons and levers. The proportions were simply scaled up… My son had to take measurements and put the multiplication skills he had learned at school into practice…

Step 2: Cutting out and gluing together
We cut out the individual pieces using a ruler and a carpet knife. This wasn’t as easy as it sounded, because the cardboard was thicker than we had expected and we wanted the edges to be as clean as possible. Once all the pieces had been cut out, we glued them together using hot melt adhesive. Hot melt adhesive was ideal because it cools quickly and creates a strong bond.

the cut-out cardboard pieces
and that’s how they are arranged
The parts are glued together with hot melt adhesive.

The finished paper machine looked pretty good, but it was still a little boring. So he painted it with his felt-tip pens and gave it a classic black design. For the buttons and levers, he made small paper knobs, which he glued to the front panel. The result was a small but detailed arcade machine made of paper.

the finished, glued-together paper arcade
the almost finished painted paper model

The idea grows: From a paper model to a functional machine
The paper model was a huge success, and we had fun making it. But at some point, I had an idea: What if we built the whole thing out of wood and equipped it with real buttons, a screen, and a small computer, such as an old Raspberry PI? That way, he could have a nice arcade machine and actually play on it. The slower days of summer vacation were perfect for this, so we began planning the construction of a real mini arcade machine.

Material selection: MDF boards and laser cutter
For the construction of the vending machine, I opted for MDF boards with a thickness of 6 mm. MDF is an ideal material for such projects: it is stable, easy to work with, and has a smooth surface that is easy to paint. It is also relatively inexpensive, which is of course a big advantage for a hobby project.

Step 1: Digitizing the sketches
The sketches of the paper vending machine served as a template for the wooden construction. I drew the designs using Inkscape software and adapted them to the dimensions of the MDF boards. I made sure that the proportions were correct and that the individual parts would fit together well later on. The final dimensions of the machine were to be approximately 25 x 30 cm—small enough to be handy, but large enough to convey an authentic arcade feel.

Inkscape files imported into the laser cutter

Step 2: Cutting with the laser cutter
The next step was to cut the MDF boards to size. A laser cutter was used for this, which was able to cut the parts with high precision. The ability to cut grooves into the boards was particularly useful here: these grooves made it possible to assemble and glue the parts together at right angles with a precise fit later on.

the laser cutter at work

Once all the parts had been cut out, I began assembling them. First, I checked that the individual parts fit together. Thanks to the precision work of the laser cutter, everything fit together perfectly. The grooves made assembly much easier, as they held the panels in the correct position while the glue dried.

everything fits together
The control panel will also be operable.

Step 3: Painting
Once the “shell” was complete, it was time to start painting. I opted for black acrylic paint to give the machine a more professional finish. The smooth surface of the MDF boards was ideal for painting, and after two coats, the machine looked almost like a miniature version of a real arcade machine.

The front glass that protects the LCD screen is made of acrylic glass. In order to paint the edge of the protective cover evenly black, the protective film on the acrylic glass is removed from the edge area. The exposed edge area of the acrylic glass is then painted black to achieve a clean and uniform appearance.

Cutting out the protective film from the acrylic glass to mask the frame

The front glass that protects the LCD screen is made of acrylic glass. In order to be able to paint the edge of the protective cover evenly black, the protective film on the acrylic glass is removed around the edges. The exposed edge area of the acrylic glass is then painted black to achieve a clean and uniform appearance. Gradually, the housing took shape and it was time to install the technical components.

The structure is slowly taking shape.

The heart of the interior is an old Raspberry PI 3 with a Retropie SD card image. For operation and control, I opted for colored push buttons with a diameter of 16 mm. Ten of these are located on the control panel. There is one button on each of the left and right sides. For convenience and cost reasons, I did not build my own controller for the Raspberry’s button operation, but used a ready-made USB-HID controller that only cost a few euros. The screen is a 5-inch 800×600 pixel LC display, and to make the Raspberry’s PCM sound output audible on a small speaker, I used a ready-made “super low-cost” Class D amplifier board.

The inner edges of the monitor frame must be blackened to prevent reflections in the acrylic panel later on.

Once all the housing parts had been painted and all the components for the interior had been prepared, we finally began assembly. Since all the housing parts are glued together but the technology must of course remain accessible, we planned an inspection door on the rear…

Keypad with colors chosen by Junior
The HID key controller finds its place
The screen with the Raspberry PI is
All components are now fully installed.

Now all that’s missing is the inspection cover on the back. To get rid of the waste heat from the Raspberry Pi, we cut a few ventilation slots into the cover. (Or, to be more precise, the laser cut them 🙂 )

Cover on the back

A 5V / 20W power supply unit is used to supply power, which is plugged into a power supply socket on the rear. It supplies all components (Raspberry, audio amplifier, and monitor).

Here is an overview of the technology used:

  • Raspberry Pi 3 with 32GB SD card
  • 5-inch LCD screen: Waveshare 5-inch 800×480
  • HID controller: USB noname joystick controller (online retailer)
  • Buttons: 16 mm momentary push buttons (set of 30 from online retailer)
  • Audio amplifier: DollaTek 3W DC 5V PAM8403 (online retailer)
  • Power supply: 5V/20W power adapter

 

from paper model to playable MiniArcade

Since I can’t think of anything sensible to write at the end of this blog post, the AI did some thinking for me and generated the following paragraph:

After weeks of hard work, the moment had finally arrived: the mini arcade machine was finished. With its black casing, colorful buttons, and small screen, it not only looks like a real arcade machine, but also plays like one. My son was thrilled—and so was I. It’s a project that not only gave us a lot of fun, but also shows how a simple idea can turn into something great.


Conclusion:
Sometimes it doesn’t take much to create something special: a little cardboard, a few MDF boards, and a large dose of enthusiasm. Our mini arcade machine is the best proof that crafting can be not only creative, but also incredibly fulfilling. Who knows, maybe this is the beginning of a whole series of mini projects? 🙂

 

Motorized TV ceiling mount control via Home Assistant

Loading

Since I’ve been working on home automation, I’ve naturally wanted to optimize and simplify as much as possible and adapt and implement it in line with the new buzzwords “green electronics”, “sustainability”, “energy-saving” … and so on. For example, my appliances switch off when they are not used or ignored, stand-by energy consumption is largely avoided and IOT technology also prevents human forgetfulness (leaving windows open in winter or forgetting to switch lights off). As readers of the blog already know, I use systems such as HomeMatic, NodeRed and, for some time now, Homeassistant with ESPHome, Zigbee2Mqtt etc. Of course, the aim is also to keep all systems cloud-free. I don’t want the data to take a detour via some server in the Far East to switch a light on and off in my home. So, if possible, everything should take place within my own network and not “phone” to the outside and also work if I cut the data line.

For a long time now, various suppliers have been offering an extremely practical device for comfort in the parents’ quiet room. I’m talking about a space-saving way of accommodating the flicker box (nowadays also known as a flat-screen TV) in the room. I’m just mentioning terms like:

Speaka Professional TV ceiling mount electric motorized (1439178) or MyWall HL46ML … etc. Some of these devices can be controlled with a wireless remote control, others via the Tuya CloudApp. You can bypass the Tuya app via the Tuya IOT development environment and bring these devices into your home assistant via the “TuyaLocal” integration – it works – but it’s more of a “ONLY” solution. In my opinion, the ideal solution is to integrate these devices into the ESPHome system. Using the Speaka Professional TV ceiling mount as an example, I will show you how it can be integrated into the ESPHome network and thus into the Home Assistant with a small extension. This version of the SpeaKa part has no Internet connection and is only controlled via a wireless remote control.

TV ceiling mount with open cover

With a little reverse engineering, we (my colleague Werner and myself) analyzed the existing appliance factory. The system is structured something like this:

Circuit board in the ceiling bracket

 

Systemdiagramm

The system diagram above shows how the circuit board is constructed. The power supply comes from a plug-in power supply with DC 24V output at 1.5A. On the board you can still see an unpopulated area whose solder pads are wired with +3V3, GND and RX, TX lines suitable for an ESP8266. A USB socket can also be seen. These two interfaces are not included in the diagram. We examined the RX/TX lines that are routed from the unpopulated solder pads (ESP8266) to the microcontroller (1301 X 016B). However, no signals could be measured here. (Presumably the interface is not activated in the flashed program version).

“Debug” wires on the RX/TX and on the RF chip

So this does not take us any further. In the next step, we looked at where the control signals of the radio remote control come from and how they are subsequently implemented. The RF receiver chip has 16 pins and unfortunately no labeling. Or has it been removed? The supply voltage of the RF chip is connected to pin 1 and pin 16, pin 2 and pin 3 are connected to a crystal and a line is routed from pin 9 to the microcontroller. So this must be the data output. Using the “PulseView” software from Sigrok and a Far East logic analyzer, we sniffed this output. And lo and behold, data packets with a duration of 10.3ms were revealed here. The PulseView software was able to recognize the protocol as an RS232 protocol after a few attempts with different analyzed data rates. It was then easy to log the received and decoded control commands to the microcontroller.

RF chip with connected “sniffer” cable

The baud rate of the RS232 port on the RF chip output is 9600 at 8N1. 10 bytes are received in HEX for each command sent. Here is the list of commands: (missing bytes follow…maybe sometime)

Befehl Byte0 Byte1 Byte2 Byte3 Byte4 Byte5 Byte6 Byte7 Byte8 Byte9
UP 0xAA 0x06 0x04 0x25 0x03 0xD5 0x01 0x00 0x02 0x55
DOWN 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x10 0x11 0x55
LEFT 0xAA 0x06 0x04 0x25 0x03 0xD5 0x55
RIGHT 0xAA 0x06 0x04 0x25 0x03 0xD5 0x55
BUTTON1 0xAA 0x06 0x04 0x25 0x03 0xD5 0x55
BUTTON2 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x08 0x09 0x55
MEM1 0xAA 0x06 0x04 0x25 0x03 0xD5 0x55
MEM2 0xAA 0x06 0x04 0x25 0x03 0xD5 0x55
OK 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x40 0x41 0x55
SET xx xx xx xx xx xx xx xx xx xx

Once the data protocol had been found using the logic analyzer, we tried to send the data to the microcontroller using a terminal program and a USB to TTL232 converter. The RF chip was removed for this purpose. It pulled the level to VCC in the idle state and prevented parallel operation of the “RS232 transmitter”.

RF-Chip removed
Board without chip with debug line

 

USB UART for sending commands

The control commands from the table above could be successfully sent via the terminal program. Now only an ESP32 board had to take over this task. An ESP32 NodeMCU board from the pool was equipped with a basic ESPHome image and integrated into the Homeassistant network. The ESPHome node now only had to be taught to send the byte sequence via the TX pin of the ESP32 when the corresponding trigger was activated in the Homeassistant. To do this, the ESP32 board was attached to the PCB and the VCC3V3, GND and TX lines were soldered to PIN9 of the former RF chip.

ESP32 on the board of the Speaka ceiling bracket

 

Re-installed in the ceiling bracket

The following esphome script must now be added to the ESPHome web environment.

 esphome:  
  name: tvhalterung  
  friendly_name: TVHalterung  
   
 esp32:  
  board: esp32dev  
  framework:  
   type: arduino  
   
 # Enable logging  
 logger:  
   
 # Enable Home Assistant API  
 api:  
  encryption:  
   key: "hier dein key beim Anlegen des device"  
   
 ota:  
  password: "hier dein ota password"  
   
 wifi:  
  ssid: !secret wifi_ssid  
  password: !secret wifi_password  
   
  # Enable fallback hotspot (captive portal) in case wifi connection fails  
  ap:  
   ssid: "Tvhalterung Fallback Hotspot"  
   password: "hier wieder deins"  
   
 captive_portal:  
   
 uart:  
  tx_pin: 4  
  rx_pin: 5  
  baud_rate: 9600  
   
 # Example button configuration  
 button:  
  - platform: template  
   name: TV Halterung UP  
   id: tv_up  
   icon: "mdi:arrow-up-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Up"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x01,0x00,0x02,0x55]  
    
  - platform: template  
   name: TV Halterung OK  
   id: tv_ok  
   icon: "mdi:stop-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV OK"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x40,0x41,0x55]  
   
  - platform: template  
   name: TV Halterung DOWN  
   id: tv_down  
   icon: "mdi:arrow-down-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Down"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x10,0x11,0x55]  
   
  - platform: template  
   name: TV Halterung Button1  
   id: tv_button1  
   icon: "mdi:numeric-1-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV Button1"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
   
  - platform: template  
   name: TV Halterung Button2  
   id: tv_button2  
   icon: "mdi:numeric-2-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV Button2"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x08,0x09,0x55]  
    
  - platform: template  
   name: TV Halterung Left  
   id: tv_left  
   icon: "mdi:arrow-left-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Left"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
   
  - platform: template  
   name: TV Halterung Right  
   id: tv_right  
   icon: "mdi:arrow-right-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Right"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
    
  - platform: template  
   name: TV Halterung MEM1  
   id: tv_mem1  
   icon: "mdi:alpha-m-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV MEM1"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]  
   
  - platform: template  
   name: TV Halterung MEM2  
   id: tv_mem2  
   icon: "mdi:alpha-m-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV MEM2"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]  

Once the esphomescript has been compiled and uploaded to the ESP, there is a new ESPHome device with the name TV holder in the Home Assistant environment. The buttons for the control are now listed here as entities. If everything went well, you should now be able to control the TV mount via the Home Assistant.

(Not all control commands have been implemented correctly yet – the correct codes will be added to the table)

LAN for Pylontech PV-batterystatus, OpenDTU and more in HomeAssistant

Loading

In the article “Pylontech PV battery status in HomeAssistant”, I had improved the project “Pylontech battery monitoring” of the following GitHub links and drew a circuit board to make the whole construct a little more compact and professional.
https://github.com/irekzielinski/Pylontech-Battery-Monitoring
https://github.com/hidaba/PylontechMonitoring
All battery data of the Pylontech battery modules are displayed in the Homeassistant. Great! But when I take a look at the list of devices registered in my wifi networks, I almost feel sick – there are now far too many wireless devices, especially from the smart home sector, sharing the channel bandwidth. So my current plan is to bring some of the smart home devices onto the wired LAN network.

edit 06.Mar.2025: you find the project on
https://github.com/ingmarsretro/pylontech_Lan_Interface

The self-made devices based on the ESPs are ideal for this. These are devices such as the OpenDTU interface, the EVU smart home interface or, as here, the interface from the serial console of the Pylontech battery to the MQTT server in the Home Assistant.

I have done some tests with boards such as the OLIMEX ESP32-PoE and the WT32-ETH01. The Olimex board would have the great advantage of also being able to be supplied with power via PoE. However, the power supply for PoE operation is so “poor” that the required standards of external boards are not met. Here I can mention the NRF24L01 radio module, for example. I did some tests with it and decided to disregard the PoE functionality for the time being. This led to the plan to use the WT32-ETH01 with an ESP32 to design a universal board with several interfaces. It should be able to do the following:

  • communicate with the PV inverters using OpenDTU and NRF24L01
  • communicate with the Smarthome system via the Pylontech Console using MQTT
  • have an optional CAN interface
  • be able to communicate via RS422/RS485 in addition to the RS232 interface
  • receive the power supply via 5V USB
  • and to have everything packed nicely small and compact in one housing

So I designed a circuit and drew a circuit board. I got the boards manufactured by a Far East PCB manufacturer. The assembly is also done quickly.

Circuit diagram of the Universal Lan Interface

The picture below shows the PCB layout before production.

The WT32-ETH01 board does not have a USB port for programming the controller. It is programmed via an external USB-UART adapter. To activate the programming mode, an IO pin must also be connected to GND. To simplify this somewhat, there is now a “PROG” jumper on the board. If this jumper is plugged in, the WT32 can receive the firmware files. I have provided a pin header slot “TO-FTDI” as a connection option for the USB-UART adapter.
The board is now designed that it can be used to operate different devices. If you connect an NRF24L01 module to the “NRF24L01+” pin header and flash the ESP32-OpenDTU image to the controller, the inverter data can be received and transmitted via the LAN network. I have created a suitable IO-config jason-file for the use of the WT32.

Another application is the use of the board with the serial output of the battery data of the Pylontech PV batteries. The batteries provide a “Console” port which represents an RS232 interface. The data is transferred to the WT32 controller via this port and is then available via LAN in the local network.

I have adapted the ESP8266 script from hidaba and irekzielinski for the ESP32 controller. (see code at the end of the article)

Once the code has been compiled and uploaded, the status of the Pylontec batteries should be visible under the set IP address after all connections have been made.

Board version with Pylontech setup
Version with OpenDTU and NRF24L01 setup

The device setups shown in the picture are equipped with a housing. I have published the “.stl” files created with FreeCad on thingiverse.

Link to thingiverse files

Here is the adapted code for use with the WT32-ETH01 board:

The “TimerConfig.h” file must be created. The file must contain the following content:

// TimerConfig.h
#ifndef TIMERCONFIG_H
#define TIMERCONFIG_H
#define TIMER_BASE_CLK 80000000
#endif // TIMERCONFIG_H

Once the file has been created, it should be located in the directory of the main program. Here is the main program:

 //Pylontec2MQTT interface  
 //code by https://github.com/irekzielinski/Pylontech-Battery-Monitoring  
 //  and https://github.com/hidaba/PylontechMonitoring  
 // the original code used WEMOS Boards with ESP8266  
 // code changed to use with WT32-ETH01 Board for use with LAN connection  
 // changes by ingmarsretro 01/2025  
 #include <ETH.h>  
 #include <WiFi.h>  
 #include <ESPmDNS.h>  
 #include <ArduinoOTA.h>  
 #include <WebServer.h>  
 #include <circular_log.h>  
 #include <ArduinoJson.h>  
 #include <NTPClient.h>  
 #include "TimerConfig.h"  
 #include <ESP32TimerInterrupt.h>  
 //+++ START CONFIGURATION +++  
 #define LED 12  
 #define RXD2 5  
 #define TXD2 17  
 #define HOSTNAME "mppsolar-pylontec"  
 #define STATIC_IP  
 IPAddress local_IP(192, 168, xx, yy);  
 IPAddress subnet(255, 255, 255, 0);  
 IPAddress gateway(192, 168, yy, zz);  
 IPAddress primaryDNS(192, 168, yy, ww);  
 //Uncomment for authentication page  
 //#define AUTHENTICATION  
 const char* www_username = "admin";  
 const char* www_password = "password";  
 //IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):  
 #define ENABLE_MQTT  
 // Set offset time in seconds to adjust for your timezone, for example:  
 // GMT +1 = 3600  
 // GMT 0 = 0  
 #define GMT 3600  
 //NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.  
 //NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc"  
 #define MQTT_SERVER    "192.168.aa.bb"  
 #define MQTT_PORT     1883  
 #define MQTT_USER     ""  
 #define MQTT_PASSWORD   ""  
 #define MQTT_TOPIC_ROOT  "ingmarsretro/pylontec/" //this is where mqtt data will be pushed  
 #define MQTT_PUSH_FREQ_SEC 2 //maximum mqtt update frequency in seconds  
 //+++  END CONFIGURATION +++  
 #ifdef ENABLE_MQTT  
 #include <PubSubClient.h>  
 WiFiClient ethClient;  
 PubSubClient mqttClient(ethClient);  
 #endif //ENABLE_MQTT  
 //text response  
 char g_szRecvBuff[7000];  
 const long utcOffsetInSeconds = GMT;  
 char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};  
 // Define NTP Client to get time  
 WiFiUDP ntpUDP;  
 NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);  
 //ESP8266WebServer server(80);  
 WebServer server(80);  
 circular_log<7000> g_log;  
 bool ntpTimeReceived = false;  
 int g_baudRate = 0;  
 void Log(const char* msg)  
 {  
  g_log.Log(msg);  
 }  
 //Define Interrupt Timer to Calculate Power meter every second (kWh)  
 #define USING_TIM_DIV1 true                       // for shortest and most accurate timer  
 //ESP8266Timer ITimer;  
 ESP32Timer ITimer(0); // Use timer 0  
 bool setInterval(unsigned long interval, timer_callback callback);   // interval (in microseconds)  
 #define TIMER_INTERVAL_MS 1000  
 //Global Variables for the Power Meter - accessible from the calculating interrupt und from main  
 unsigned long powerIN = 0;    //WS gone in to the BAttery  
 unsigned long powerOUT = 0;   //WS gone out of the Battery  
 //Global Variables for the Power Meter - Überlauf  
 unsigned long powerINWh = 0;    //WS gone in to the BAttery  
 unsigned long powerOUTWh = 0;   //WS gone out of the Battery  
 void setup() {  
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); //clean variable  
  pinMode(LED, OUTPUT);   
  //digitalWrite(LED, HIGH);//high is off  
  digitalWrite(LED, LOW);//low is off  
  // put your setup code here, to run once:  
  //WiFi.mode(WIFI_STA);  
  //WiFi.persistent(false); //our credentialss are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)  
  //WiFi.hostname(HOSTNAME);  
  ETH.begin();  
  ETH.setHostname(HOSTNAME);  
   #ifdef STATIC_IP  
    ETH.config(local_IP, gateway, subnet, primaryDNS);  
  #endif  
  for(int ix=0; ix<10; ix++)  
  {  
   Log("Wait for LAN Connection");  
   if (ETH.linkUp()) {  
     Serial2.println("Ethernet connected");  
     Serial2.print("IP Address: ");  
     Serial2.println(ETH.localIP());  
   } else {  
     Serial2.println("Failed to connect to Ethernet");  
   }  
   delay(1000);  
  }  
  ArduinoOTA.setHostname(HOSTNAME);  
  ArduinoOTA.begin();  
  server.on("/", handleRoot);  
  server.on("/log", handleLog);  
  server.on("/req", handleReq);  
  server.on("/jsonOut", handleJsonOut);  
  server.on("/reboot", [](){  
   #ifdef AUTHENTICATION  
   if (!server.authenticate(www_username, www_password)) {  
    return server.requestAuthentication();  
   }  
   #endif  
   ESP.restart();  
  });  
  server.begin();   
  timeClient.begin();  
 #ifdef ENABLE_MQTT  
  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);  
 #endif  
  Log("Boot event");  
 }  
 void handleLog()  
 {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }   
  #endif  
  server.send(200, "text/html", g_log.c_str());  
 }  
 void switchBaud(int newRate)  
 {  
  if(g_baudRate == newRate)  
  {  
   return;  
  }  
  if(g_baudRate != 0)  
  {  
   Serial2.flush();  
   delay(20);  
   Serial2.end();  
   delay(20);  
  }  
  char szMsg[50];  
  snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate);  
  Log(szMsg);  
  Serial2.begin(newRate,SERIAL_8N1, RXD2, TXD2);  
  g_baudRate = newRate;  
  delay(20);  
 }  
 void waitForSerial()  
 {  
  for(int ix=0; ix<150;ix++)  
  {  
   if(Serial2.available()) break;  
   delay(10);  
  }  
 }  
 int readFromSerial()  
 {  
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));  
  int recvBuffLen = 0;  
  bool foundTerminator = true;  
  waitForSerial();  
  while(Serial2.available())  
  {  
   char szResponse[256] = "";  
   const int readNow = Serial2.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end)  
   if(readNow > 0 &&   
     szResponse[0] != '\0')  
   {  
    if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff)))  
    {  
     Log("WARNING: Read too much data on the console!");  
     break;  
    }  
    strcat(g_szRecvBuff, szResponse);  
    recvBuffLen += readNow;  
    if(strstr(g_szRecvBuff, "$$\r\n\rpylon"))  
    {  
     strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add  
     foundTerminator = true;  
     break; //found end of the string  
    }  
    if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit"))  
    {  
     //we need to send new line character so battery continues the output  
     Serial2.write("\r");  
    }  
    waitForSerial();  
   }  
  }  
  if(recvBuffLen > 0 )  
  {  
   if(foundTerminator == false)  
   {  
    Log("Failed to find pylon> terminator");  
   }  
  }  
  return recvBuffLen;  
 }  
 bool readFromSerialAndSendResponse()  
 {  
  const int recvBuffLen = readFromSerial();  
  if(recvBuffLen > 0)  
  {  
   server.sendContent(g_szRecvBuff);  
   return true;  
  }  
  return false;  
 }  
 bool sendCommandAndReadSerialResponse(const char* pszCommand)  
 {  
  switchBaud(115200);  
  if(pszCommand[0] != '\0')  
  {  
   Serial2.write(pszCommand);  
  }  
  Serial2.write("\n");  
  const int recvBuffLen = readFromSerial();  
  if(recvBuffLen > 0)  
  {  
   return true;  
  }  
  //wake up console and try again:  
  wakeUpConsole();  
  if(pszCommand[0] != '\0')  
  {  
   Serial2.write(pszCommand);  
  }  
  Serial2.write("\n");  
  return readFromSerial() > 0;  
 }  
 void handleReq()  
 {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }  
  #endif  
  bool respOK;  
  if(server.hasArg("code") == false)  
  {  
   respOK = sendCommandAndReadSerialResponse("");  
  }  
  else  
  {  
   respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str());  
  }  
  handleRoot();  
 }  
 void handleJsonOut()  
 {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }  
  #endif  
  if(sendCommandAndReadSerialResponse("pwr") == false)  
  {  
   server.send(500, "text/plain", "Failed to get response to 'pwr' command");  
   return;  
  }  
  parsePwrResponse(g_szRecvBuff);  
  prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));  
  server.send(200, "application/json", g_szRecvBuff);  
 }  
 void handleRoot() {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }  
  #endif  
  timeClient.update(); //get ntp datetime  
  unsigned long days = 0, hours = 0, minutes = 0;  
  unsigned long val = os_getCurrentTimeSec();  
  days = val / (3600*24);  
  val -= days * (3600*24);  
  hours = val / 3600;  
  val -= hours * 3600;  
  minutes = val / 60;  
  val -= minutes*60;  
  time_t epochTime = timeClient.getEpochTime();  
  String formattedTime = timeClient.getFormattedTime();  
  //Get a time structure  
  struct tm *ptm = gmtime ((time_t *)&epochTime);   
  int currentMonth = ptm->tm_mon+1;  
  static char szTmp[9500] = "";   
  long timezone= GMT / 3600;  
  snprintf(szTmp, sizeof(szTmp)-1, "<html><b>Pylontech Battery LAN Interface</b><br>Time GMT: %s (%s %d)<br>Uptime: %02d:%02d:%02d.%02d<br><br>free heap: %u<br>MQTT-Server: %s<br>MQTT-root topic: %s",   
       formattedTime, "GMT ", timezone,  
       (int)days, (int)hours, (int)minutes, (int)val,   
       ESP.getFreeHeap(),MQTT_SERVER,MQTT_TOPIC_ROOT);  
  strncat(szTmp, "<BR><a href='/log'>Runtime log</a><HR>", sizeof(szTmp)-1);  
  strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'> <a href='/req?code=pwr'>PWR</a> | <a href='/req?code=pwr%201'>Power 1</a> | <a href='/req?code=pwr%202'>Power 2</a> | <a href='/req?code=pwr%203'>Power 3</a> | <a href='/req?code=pwr%204'>Power 4</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);  
  //strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'><a href='/req?code=pwr'>Power</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);  
  strncat(szTmp, "<textarea rows='80' cols='180'>", sizeof(szTmp)-1);  
  //strncat(szTmp, "<textarea rows='45' cols='180'>", sizeof(szTmp)-1);  
  strncat(szTmp, g_szRecvBuff, sizeof(szTmp)-1);  
  strncat(szTmp, "</textarea></form>", sizeof(szTmp)-1);  
  strncat(szTmp, "</html>", sizeof(szTmp)-1);  
  //send page  
  server.send(200, "text/html", szTmp);  
 }  
 unsigned long os_getCurrentTimeSec()  
 {  
  static unsigned int wrapCnt = 0;  
  static unsigned long lastVal = 0;  
  unsigned long currentVal = millis();  
  if(currentVal < lastVal)  
  {  
   wrapCnt++;  
  }  
  lastVal = currentVal;  
  unsigned long seconds = currentVal/1000;  
  //millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter  
  return (wrapCnt*4294967) + seconds;  
 }  
 void wakeUpConsole()  
 {  
  switchBaud(1200);  
  //byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};  
  //Serial.write(wakeUpBuff, sizeof(wakeUpBuff));  
  Serial.write("~20014682C0048520FCC3\r");  
  delay(1000);  
  byte newLineBuff[] = {0x0E, 0x0A};  
  switchBaud(115200);  
  for(int ix=0; ix<10; ix++)  
  {  
   Serial.write(newLineBuff, sizeof(newLineBuff));  
   delay(1000);  
   if(Serial.available())  
   {  
    while(Serial.available())  
    {  
     Serial.read();  
    }  
    break;  
   }  
  }  
 }  
 #define MAX_PYLON_BATTERIES 8  
 struct pylonBattery  
 {  
  bool isPresent;  
  long soc;   //Coulomb in %  
  long voltage; //in mW  
  long current; //in mA, negative value is discharge  
  long tempr;  //temp of case or BMS?  
  long cellTempLow;  
  long cellTempHigh;  
  long cellVoltLow;  
  long cellVoltHigh;  
  char baseState[9];  //Charge | Dischg | Idle  
  char voltageState[9]; //Normal  
  char currentState[9]; //Normal  
  char tempState[9];  //Normal  
  char time[20];    //2019-06-08 04:00:29  
  char b_v_st[9];    //Normal (battery voltage?)  
  char b_t_st[9];    //Normal (battery temperature?)  
  bool isCharging()  const { return strcmp(baseState, "Charge")  == 0; }  
  bool isDischarging() const { return strcmp(baseState, "Dischg")  == 0; }  
  bool isIdle()    const { return strcmp(baseState, "Idle")   == 0; }  
  bool isBalancing()  const { return strcmp(baseState, "Balance") == 0; }  
  bool isNormal() const  
  {  
   if(isCharging()  == false &&  
     isDischarging() == false &&  
     isIdle()    == false &&  
     isBalancing()  == false)  
   {  
    return false; //base state looks wrong!  
   }  
   return strcmp(voltageState, "Normal") == 0 &&  
       strcmp(currentState, "Normal") == 0 &&  
       strcmp(tempState,  "Normal") == 0 &&  
       strcmp(b_v_st,    "Normal") == 0 &&  
       strcmp(b_t_st,    "Normal") == 0 ;  
  }  
 };  
 struct batteryStack  
 {  
  int batteryCount;  
  int soc; //in %, if charging: average SOC, otherwise: lowest SOC  
  int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest  
  long currentDC;  //mAh current going in or out of the battery  
  long avgVoltage;  //in mV  
  char baseState[9]; //Charge | Dischg | Idle | Balance | Alarm!  
  pylonBattery batts[MAX_PYLON_BATTERIES];  
  bool isNormal() const  
  {  
   for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)  
   {  
    if(batts[ix].isPresent &&   
      batts[ix].isNormal() == false)  
    {  
     return false;  
    }  
   }  
   return true;  
  }  
  //in Wh  
  long getPowerDC() const  
  {  
   return (long)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));  
  }  
  // power in Wh in charge  
  float powerIN() const  
  {  
   if (currentDC > 0) {  
     return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));  
   } else {  
     return (float)(0);  
   }  
  }  
  // power in Wh in discharge  
  float powerOUT() const  
  {  
   if (currentDC < 0) {  
     return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0)*-1);  
   } else {  
     return (float)(0);  
   }  
  }  
  //Wh estimated current on AC side (taking into account Sofar ME3000SP losses)  
  long getEstPowerAc() const  
  {  
   double powerDC = (double)getPowerDC();  
   if(powerDC == 0)  
   {  
    return 0;  
   }  
   else if(powerDC < 0)  
   {  
    //we are discharging, on AC side we will see less power due to losses  
    if(powerDC < -1000)  
    {  
     return (long)(powerDC*0.94);  
    }  
    else if(powerDC < -600)  
    {  
     return (long)(powerDC*0.90);  
    }  
    else  
    {  
     return (long)(powerDC*0.87);  
    }  
   }  
   else  
   {  
    //we are charging, on AC side we will have more power due to losses  
    if(powerDC > 1000)  
    {  
     return (long)(powerDC*1.06);  
    }  
    else if(powerDC > 600)  
    {  
     return (long)(powerDC*1.1);  
    }  
    else  
    {  
     return (long)(powerDC*1.13);  
    }  
   }  
  }  
 };  
 batteryStack g_stack;  
 long extractInt(const char* pStr, int pos)  
 {  
  return atol(pStr+pos);  
 }  
 void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)  
 {  
  strOut[strOutSize-1] = '\0';  
  strncpy(strOut, pStr+pos, strOutSize-1);  
  strOutSize--;  
  //trim right  
  while(strOutSize > 0)  
  {  
   if(isspace(strOut[strOutSize-1]))  
   {  
    strOut[strOutSize-1] = '\0';  
   }  
   else  
   {  
    break;  
   }  
   strOutSize--;  
  }  
 }  
 /* Output has mixed \r and \r\n  
 pwr  
 @  
 Power Volt  Curr  Tempr Tlow  Thigh Vlow  Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time         B.V.St  B.T.St   
 1   49735 -1440 22000 19000 19000 3315  3317  Dischg  Normal  Normal  Normal  93%   2019-06-08 04:00:30 Normal  Normal   
 ....    
 8   -   -   -   -   -   -   -   Absent  -    -    -    -    -          -    -      
 Command completed successfully  
 $$  
 pylon  
 */  
 bool parsePwrResponse(const char* pStr)  
 {  
  if(strstr(pStr, "Command completed successfully") == NULL)  
  {  
   return false;  
  }  
  int chargeCnt  = 0;  
  int dischargeCnt = 0;  
  int idleCnt   = 0;  
  int alarmCnt   = 0;  
  int socAvg    = 0;  
  int socLow    = 0;  
  int tempHigh   = 0;  
  int tempLow   = 0;  
  memset(&g_stack, 0, sizeof(g_stack));  
  for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)  
  {  
   char szToFind[32] = "";  
   snprintf(szToFind, sizeof(szToFind)-1, "\r\r\n%d   ", ix+1);  
   const char* pLineStart = strstr(pStr, szToFind);  
   if(pLineStart == NULL)  
   {  
    return false;  
   }  
   pLineStart += 3; //move past \r\r\n  
   extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));  
   if(strcmp(g_stack.batts[ix].baseState, "Absent") == 0)  
   {  
    g_stack.batts[ix].isPresent = false;  
   }  
   else  
   {  
    g_stack.batts[ix].isPresent = true;  
    extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));  
    extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));  
    extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));  
    extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));  
    extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));  
    extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));  
    g_stack.batts[ix].voltage = extractInt(pLineStart, 6);  
    g_stack.batts[ix].current = extractInt(pLineStart, 13);  
    g_stack.batts[ix].tempr  = extractInt(pLineStart, 20);  
    g_stack.batts[ix].cellTempLow  = extractInt(pLineStart, 27);  
    g_stack.batts[ix].cellTempHigh  = extractInt(pLineStart, 34);  
    g_stack.batts[ix].cellVoltLow  = extractInt(pLineStart, 41);  
    g_stack.batts[ix].cellVoltHigh  = extractInt(pLineStart, 48);  
    g_stack.batts[ix].soc      = extractInt(pLineStart, 91);  
    //////////////////////////////// Post-process ////////////////////////  
    g_stack.batteryCount++;  
    g_stack.currentDC += g_stack.batts[ix].current;  
    g_stack.avgVoltage += g_stack.batts[ix].voltage;  
    socAvg += g_stack.batts[ix].soc;  
    if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }  
    else if(g_stack.batts[ix].isCharging()){chargeCnt++;}  
    else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}  
    else if(g_stack.batts[ix].isIdle()){idleCnt++;}  
    else{ alarmCnt++; } //should not really happen!  
    if(g_stack.batteryCount == 1)  
    {  
     socLow = g_stack.batts[ix].soc;  
     tempLow = g_stack.batts[ix].cellTempLow;  
     tempHigh = g_stack.batts[ix].cellTempHigh;  
    }  
    else  
    {  
     if(socLow > g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}  
     if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}  
     if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}  
    }  
   }  
  }  
  //now update stack state:  
  g_stack.avgVoltage /= g_stack.batteryCount;  
  g_stack.soc = socLow;  
  if(tempHigh > 15000) //15C  
  {  
   g_stack.temp = tempHigh; //in the summer we highlight the warmest cell  
  }  
  else  
  {  
   g_stack.temp = tempLow; //in the winter we focus on coldest cell  
  }  
  if(alarmCnt > 0)  
  {  
   strcpy(g_stack.baseState, "Alarm!");  
  }  
  else if(chargeCnt == g_stack.batteryCount)  
  {  
   strcpy(g_stack.baseState, "Charge");  
   g_stack.soc = (int)(socAvg / g_stack.batteryCount);  
  }  
  else if(dischargeCnt == g_stack.batteryCount)  
  {  
   strcpy(g_stack.baseState, "Dischg");  
  }  
  else if(idleCnt == g_stack.batteryCount)  
  {  
   strcpy(g_stack.baseState, "Idle");  
  }  
  else  
  {  
   strcpy(g_stack.baseState, "Balance");  
  }  
  return true;  
 }  
 void prepareJsonOutput(char* pBuff, int buffSize)  
 {  
  memset(pBuff, 0, buffSize);  
  snprintf(pBuff, buffSize-1, "{\"soc\": %d, \"temp\": %d, \"currentDC\": %ld, \"avgVoltage\": %ld, \"baseState\": \"%s\", \"batteryCount\": %d, \"powerDC\": %ld, \"estPowerAC\": %ld, \"isNormal\": %s}", g_stack.soc,   
                                                                                                       g_stack.temp,   
                                                                                                       g_stack.currentDC,   
                                                                                                       g_stack.avgVoltage,   
                                                                                                       g_stack.baseState,   
                                                                                                       g_stack.batteryCount,   
                                                                                                       g_stack.getPowerDC(),   
                                                                                                       g_stack.getEstPowerAc(),  
                                                                                                       g_stack.isNormal() ? "true" : "false");  
 }  
 void loop() {  
 #ifdef ENABLE_MQTT  
  mqttLoop();  
 #endif  
  ArduinoOTA.handle();  
  server.handleClient();  
  //if there are bytes availbe on serial here - it's unexpected  
  //when we send a command to battery, we read whole response  
  //if we get anything here anyways - we will log it  
  int bytesAv = Serial.available();  
  if(bytesAv > 0)  
  {  
   if(bytesAv > 63)  
   {  
    bytesAv = 63;  
   }  
   char buff[64+4] = "RCV:";  
   if(Serial.readBytes(buff+4, bytesAv) > 0)  
   {  
    //digitalWrite(LED, LOW);  
    digitalWrite(LED, HIGH);  
    delay(5);  
    //digitalWrite(LED, HIGH);//high is off  
    digitalWrite(LED, LOW);  
    Log(buff);  
   }  
  }  
 }  
 #ifdef ENABLE_MQTT  
 #define ABS_DIFF(a, b) (a > b ? a-b : b-a)  
 void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)  
 {  
  char szTmp[16] = "";  
  snprintf(szTmp, 15, "%.2f", newValue);  
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)  
  {  
   mqttClient.publish(topic, szTmp, false);  
  }  
 }  
 void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)  
 {  
  char szTmp[16] = "";  
  snprintf(szTmp, 15, "%d", newValue);  
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)  
  {  
   mqttClient.publish(topic, szTmp, false);  
  }  
 }  
 void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)  
 {  
  if(force || strcmp(newValue, oldValue) != 0)  
  {  
   mqttClient.publish(topic, newValue, false);  
  }  
 }  
 void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */)  
 {  
  mqtt_publish_f(MQTT_TOPIC_ROOT "soc",     g_stack.soc,        lastSentData.soc,        0, forceUpdate);  
  mqtt_publish_f(MQTT_TOPIC_ROOT "temp",     (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0.1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "currentDC",  g_stack.currentDC,     lastSentData.currentDC,     1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC",  g_stack.getEstPowerAc(),  lastSentData.getEstPowerAc(),  10, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount,    lastSentData.batteryCount,    0, forceUpdate);  
  mqtt_publish_s(MQTT_TOPIC_ROOT "base_state",  g_stack.baseState,     lastSentData.baseState      , forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal",  g_stack.isNormal() ? 1:0,  lastSentData.isNormal() ? 1:0,  0, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "getPowerDC",  g_stack.getPowerDC(),    lastSentData.getPowerDC(),    1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerIN",   g_stack.powerIN(),     lastSentData.powerIN(),     1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerOUT",   g_stack.powerOUT(),     lastSentData.powerOUT(),     1, forceUpdate);  
  // publishing details  
  for (int ix = 0; ix < g_stack.batteryCount; ix++) {  
   char ixBuff[50];  
   String ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/voltage";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_f(ixBuff, g_stack.batts[ix].voltage / 1000.0, lastSentData.batts[ix].voltage / 1000.0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/current";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_f(ixBuff, g_stack.batts[ix].current / 1000.0, lastSentData.batts[ix].current / 1000.0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/soc";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].soc, lastSentData.batts[ix].soc, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/charging";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].isCharging()?1:0, lastSentData.batts[ix].isCharging()?1:0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/discharging";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].isDischarging()?1:0, lastSentData.batts[ix].isDischarging()?1:0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/idle";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].isIdle()?1:0, lastSentData.batts[ix].isIdle()?1:0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/state";  
   ixBattStr.toCharArray(ixBuff, 50);  
   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);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/temp";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_f(ixBuff, (float)g_stack.batts[ix].tempr/1000.0, (float)lastSentData.batts[ix].tempr/1000.0, 0.1, forceUpdate);  
  }  
 }   
 void mqttLoop()  
 {  
  //if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)  
  static unsigned long g_lastConnectionAttempt = 0;  
  //first: let's make sure we are connected to mqtt  
  const char* topicLastWill = MQTT_TOPIC_ROOT "availability";  
  if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) {  
   if(mqttClient.connect(HOSTNAME, MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline"))  
   {  
    Log("Connected to MQTT server: " MQTT_SERVER);  
    mqttClient.publish(topicLastWill, "online", true);  
   }  
   else  
   {  
    Log("Failed to connect to MQTT server.");  
   }  
   g_lastConnectionAttempt = os_getCurrentTimeSec();  
  }  
  //next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)  
  static unsigned long g_lastDataSent = 0;  
  if(mqttClient.connected() &&   
    os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC &&  
    sendCommandAndReadSerialResponse("pwr") == true)  
  {  
   static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again  
   static unsigned int callCnt = 0;  
   parsePwrResponse(g_szRecvBuff);  
   bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call  
   pushBatteryDataToMqtt(lastSentData, forceUpdate);  
   callCnt++;  
   g_lastDataSent = os_getCurrentTimeSec();  
   memcpy(&lastSentData, &g_stack, sizeof(batteryStack));  
  }  
  mqttClient.loop();  
 }  
 #endif //ENABLE_MQTT  

When the brush stops turning – or: Repairing the Kärcher FC7

Loading

“Freshly mopped floors without having to vacuum beforehand: The FC 7 Cordless hard floor cleaner removes all types of dry and damp everyday dirt in one step.” (Original text kaercher.com)

You get this product promise on the manufacturer’s website if you are interested in the FC7 electric hard floor cleaner. However, when this promise is no longer kept, I find out about the existence of these appliances. Because then I am asked to check why something is no longer working as it should. This is also the case here. The brushes (rollers – whatever these parts are called) no longer rotate, according to the problem description. Or to be more precise, they only turn sometimes when the bottom part of the moving handle is in a certain position. And since the handle (in which all the electronics such as batteries, BMS and operating elements are housed) can be moved within a wide range, it is reasonable to assume that there is a cable break or similar contact problem.

This is not exactly a complex problem, but perhaps one or the other is interested in how the problem can be solved with more or less effort.

The first step is to remove the wastewater tank and the four cleaning rollers. The screws on the drive cover and battery cover can then be loosened and the covers removed.

Screws of the drive cover
Battery/electronics cover screws

Once the covers have been loosened, they can be removed. The circuit board with the BMS and the control electronics of the device can be seen under the battery cover. The 18650 Li-Ion cells are located underneath. The outlets to the bottom drive, to the control unit in the handle, etc. are plugged in.

Circuit board with BMS and control unit

The eight-pin plug at the bottom left of the picture must be disconnected. It connects the brush drive to the electronics. Six of the eight pins of the plug are occupied. One red and one black wire are used to supply the DC motor (yes, only a DC brush motor has been installed here and not a brushless one …) and two brown wires are laid to the pins that form the resistance sensor for the water level in the dirty water tank. Two blue wires control the solenoid valve of the water inlet.

As the fault is in the motor drive (depending on the position of the handle, the motor may or may not turn), the fault may be in the cable connection from the circuit board to the motor. The fault was quickly discovered with the continuity test of the multimeter. The black cable to the motor was broken.

Broken cable (black wire to DC motor)

The breaking point is exactly in the area where the handle is movably attached to the floor unit. This is exactly where the wiring harness and the rubber hose for the water guide are inserted. Constantly moving the cable harness will inevitably damage and break the cables in the long term. Especially if the handle is used at very shallow angles, for example to clean the floor under boxes, chests of drawers, etc.

Wire soldered and insulated with heat-shrink tubing

I did the repair here by soldering the wire together and protecting it with heat-shrink tubing. I wrapped the damaged cable protection conduit with insulating tape. This should hold for some time. As the part will not last forever due to its design, the wiring harness should be completely replaced during the next repair. (as this is probably not available as a spare part, you will probably have to make one yourself – but then with more stable, highly flexible wires…)

The components could now be reassembled. Place the rollers in the green/blue colors on the drive hubs and screw everything back together.

Broken cable connections and torn toothed belts in the motor unit are obviously the most common faults with this appliance.

 

Geiger counter – kit from the Far East

Loading

II am always fascinated by the topic of radioactivity. More precisely, it is the measurement or detection of this ionizing radiation, which is produced by the decay and of atomic nuclei with the release of energy. A basic distinction is made between the energy (alpha and beta particles) emitted by the movement of the decaying particles (i.e. particle radiation) and the radiation energy that is transported as an electromagnetic wave (gamma radiation and also X-rays). These types of radiation have different energy densities and ranges. Depending on the type, they are more or less easy to shield. Alpha radiation is particle radiation that is strongly slowed down by matter (air, water) and no longer penetrates a sheet of paper. However, these particles give off the energy over their very short distance. This is particularly dangerous if these particles are inhaled or radiate on the upper layers of the skin. Gamma radiation in turn penetrates matter very easily like a radio wave and can be shielded most effectively with lead. It goes without saying that this type of radiation is anything but harmless.

You cannot see, smell, taste or otherwise perceive this radiation directly, but the danger is still there. With relatively simple techniques, however, these decay processes can be made visible or audible and counted.

This has been done for a long time with a so-called counter tube or, thanks to modern technology, with semiconductors. A P-N junction is operated in reverse direction and the very small reverse current is measured with the exclusion of light (i.e. darkened). If high-energy radiation hits this P-N transition, the current flow is increased for a short time and can be detected.

Whenever the opportunity arises to get a detector very cheaply, I of course take it. So this time too. I had to look at a simple kit based on detection using a counter tube. The kit comes from the Far East and consists of a base board, an attached Arduino Nano and an LC display that is also attached.

All components required for detection are on the mainboard. This includes, among other things, the generation of high voltage for the counter tube, which is implemented using a simple boost converter circuit driven by a 555. To attach the counter tube to the mainboard, the designer of this board chose simple glass tube fuse holders. They don’t fit exactly, but they can be stretched so that they hold the counter tube firmly in place. Incidentally, the counter tube is a J305. It is approx. 90mm long and has a diameter of almost one centimeter.

The counter tube works with an anode voltage of 350V to 480V. Below I have listed the specifications from the data sheet:

  • Anode voltage: 350 v bis 480 V
  • Type: J305 Geiger-counter tube
  • Cathode material: tin oxide
  • Wall density: 50 ± 10 cg/cm²
  • Operating temperature range: -40 °C bis 50 °C
  • Diameter: 10 mm (±0,5 mm)
  • Length: 90 mm (±2 mm)
  • Self-background radiation: 0,2 pulses/s
  • Sensivity to γ-radiation: 0,1 MeV
  • Current consumption: 0,015 mA bis 0,02 mA
  • Working voltage: 380 V bis 450 V
  • γ-radiation: 20mR/h ~ 120mR/h
  • β-radiation: 100 ~ 1800 Pulse/min.
  •  100 ~ 1800 pulses/min.

The signal detection and processing of the signal also takes place on the mainboard. The recognized impulses are reproduced via a small piezo loudspeaker. In order to be able to count them, you don’t have to sit in front of the loudspeaker with a stopwatch and count the beeps every minute – no – that is done by a microcontroller, which, as is common today, consists of a finished board. Here the designer has chosen an Arduino Nano (or nano replica). In turn, a program runs on it that counts the impulses and also shows them nicely on a two-line LC display and ideally also converts them into µSievert / h. To transfer the pulses to the Arduino, the level of the signal is brought to TTL level and switched to the interrupt input of the Arduino. The LC display uses the I2C output of the Arduino. The lines for this are only led from the socket strip into which the Arduino is plugged via the mainboard to the socket strip for the display. To supply the whole system with voltage, the 5V from the USB port of the Arduino are used directly. Optionally, the 5V can also be connected to the mainboard via a connector strip.

Once everything has been assembled and the USB supply is connected, there is first of all a short waiting time during which the high voltage is built up. Here the programmer has come up with an animation that shows “Boot …” on the display.

And then it starts. The Geiger counter is ready for use and begins to count. As a test I only have an old clock with hands painted with radium paint. There is at least a clear change in the number of detected counting pulses when the watch is brought near the counter tube.

Pylontech PV battery status in the HomeAssistant

Loading

Anyone who has installed a photovoltaic system in their own home may even use an energy storage system. In this example, it is an off-grid system equipped with two modules from the manufacturer Pylontech. The Pylontech US3000C batteries have an output voltage of 48V. The nominal capacity is 3500Wh. The installed cells are LiFePO4 cells and the usable capacity is specified as 3374Wh according to the data sheet. The batteries are designed to be connected in parallel with other Pylontech batteries. The internally installed BMS (battery management system) communicates with the other Pylontech battery modules via a so-called “link” interface. A battery configured as a “master” handles the data exchange with the inverter. Here, Pylontech provides the CAN or RS485 bus as an interface. However, if you want information about the individual cells (voltages, currents, charges, temperatures, etc.), there is another interface on each module called “Console”. This is an RS232 interface via which you can communicate directly with the battery’s BMS. This port is also used to update the firmware of the BMS. However, I STRONGLY advise against playing around with firmware updates and flash software. This is reserved for the manufacturer or the liable party.

However, as this interface also provides a lot of information about the cells installed in the battery, this is an interesting approach. Initially, I had a laptop connected to a terminal and was able to discover and monitor the individual cell voltages and, above all, the possibly different charge status of the modules connected in parallel. So I thought it would be a good idea to have this information available in my home automation system, where it could be visualized and used for control purposes.

As we geeks and technology enthusiasts are quite familiar with terms such as Homeassistant, Docker, Proxmox, HomeMatic, NodeRed etc., I thought that this data should also become entities in the Homeassistant. So a small new project was quickly created. My plan was to read the data from the serial interface and send it to the Home Assistant via MQTT.

But before I start disassembling the data strings that come out via the serial port, I’ll have a look at the search engines. Perhaps someone else has already dealt with this topic. And that’s exactly what happened. I found what I was looking for on GitHub under the term “pylontec2mqtt”. A project is hosted at https://github.com/irekzielinski/Pylontech-Battery-Monitoring that uses ESP8266 to collect the serial data from the port and sends it to the Homeassistant server via MQTT and Wifi. A fork with a further development of this project can be found at https://github.com/hidaba/PylontechMonitoring.

Why am I publishing the project here on the blog despite the simple replica? I have optimized the circuit a little and packed it into a layout and adapted the code a little. I would like to share the result here. It was important to me to have a sensible structure on a circuit board that is connected with a USB A-B cable for the power supply and a LAN-RJ45 cable for the data connection. I wanted to use a “solid” USB connector (not the fragile mini or micro USB connectors)

On a breadboard and with the usual development boards, I quickly “knitted together” a functional model so that I could adapt the software to it.

Functional sample on perforated grid

So I first created a circuit diagram from the sketches in the Git project. There is a “real” RS232 level at the “Console” interface, which is converted to a 5V TTL via the MAX3232 IC. The BSS123 FET is used to realize a level converter to 3.3V for each of the RX and TX signals.

pylontec2mqtt schematic

The ESP8266 processes this 3.3V TTL level in the form of the Wemos D1Mini or WemosD1Pro development board, which is plugged onto the circuit board. I then packed the entire construction into a small plastic housing, which can be conveniently connected to the Pylontec and a USB power source via the LAN and USB cables.

Layout preview in designtool

The layout design is shown in the picture above. The circuit board and the position of the components were checked again with the preview before production and then ordered from a trusted manufacturer.

Preview of the circuit board before production

After barely two weeks of waiting, I had the empty circuit boards in my hands and was able to fit them with the components.

fully assembled circuit board

The picture above shows the fully assembled board. The only thing missing here is the Wemos board with the ESP.

Comparison between functional model and first “production model”

In the end, I plugged in a WemosD1 Pro, as this offers the option of connecting an external WiFi antenna and thus getting a reasonable wireless range.

After flashing the software and commissioning, the Wemos web server can be accessed at the IP address specified in the code. Here you can also check whether the Pylontech battery is communicating with the Wemos. The result then looks like this.

Webseite of the WEMOS ESP

Here you can see that both battery modules are recognized correctly. The next step is to check whether messages are being sent via the MQTT protocol. The IP address of the MQTT broker must also be specified in the Wemo code. In my setup, I have set up the MQTT Explorer in the Home Assistant to be able to check the MQTT functions quickly and easily.

MQTT Explorer

The image above shows that the data also arrives correctly via MQTT. Now it is only necessary to create a sensor yaml file in the home assistant to make the topics available as entities. I have added the following code to configuration.yaml for this purpose:

mqtt:
  sensor:
#Pylontec Akku Serial Readout (ESP32 192.168.xxx.yyy)
    - state_topic: "ingmarsretro/pylontec/ESP_WiFi_RSSI"
      name: "Pylontec_RSSI"
      unit_of_measurement: dB
      
    - state_topic: "ingmarsretro/pylontec/availability"
      name: "Pylontec_Status"
      
    - state_topic: "ingmarsretro/pylontec/currentDC"
      name: "DC-Strom"
      unit_of_measurement: "mA"
      
    - state_topic: "ingmarsretro/pylontec/getPowerDC"  
      name: "getPower DC"
      unit_of_measurement: "W"
      
    - state_topic: "ingmarsretro/pylontec/powerIN"  
      name: "Power IN"
      unit_of_measurement: "W"  
      
    - state_topic: "ingmarsretro/pylontec/estPowerAC"
      name: "Pylontec_estPowerAC"
      unit_of_measurement: Watt  
      
    - state_topic: "ingmarsretro/pylontec/soc"
      name: "Pylontec_SOC"
      unit_of_measurement: "%"  
      
    - state_topic: "ingmarsretro/pylontec/temp"
      name: "Pylontec_temperature"
      unit_of_measurement: "°C"
      
    - state_topic: "ingmarsretro/pylontec/battery_count"
      name: "Pylontec_BatteryCount"
      unit_of_measurement: "pcs"      
      
    - state_topic: "ingmarsretro/pylontec/base_state"
      name: "Pylontec_BaseState"
      
    - state_topic: "ingmarsretro/pylontec/is_normal"  
      name: "Pylontec_is_normal"

    - state_topic: "ingmarsretro/pylontec/powerOUT"
      name: "Pylontec_powerOUT"
      unit_of_measurement: Watt
      
# Pylontech battery module 0      
      
    - state_topic: "ingmarsretro/pylontec/0/current"
      name: "Pylontec_Battery0_current"
      unit_of_measurement: "A"

    - state_topic: "ingmarsretro/pylontec/0/voltage"
      name: "Pylontec_Battery0_voltage"
      unit_of_measurement: "V"
      
    - state_topic: "ingmarsretro/pylontec/0/soc"
      name: "Pylontec_Battery0_soc"
      unit_of_measurement: "%"
      
    - state_topic: "ingmarsretro/pylontec/0/charging"
      name: "Pylontec_Battery0_charging"
      
    - state_topic: "ingmarsretro/pylontec/0/discharging"
      name: "Pylontec_Battery0_discharging"
     
    - state_topic: "ingmarsretro/pylontec/0/idle"
      name: "Pylontec_Battery0_idle"
      
    - state_topic: "ingmarsretro/pylontec/0/state"
      name: "Pylontec_Battery0_state"
      
    - state_topic: "ingmarsretro/pylontec/0/temp"
      name: "Pylontec_Battery0_temp"
      unit_of_measurement: "°C"
      
# Pylontech battery module 1      
      
    - state_topic: "ingmarsretro/pylontec/1/current"
      name: "Pylontec_Battery1_current"
      unit_of_measurement: "A"

    - state_topic: "ingmarsretro/pylontec/1/voltage"
      name: "Pylontec_Battery1_voltage"
      unit_of_measurement: "V"
      
    - state_topic: "ingmarsretro/pylontec/1/soc"
      name: "Pylontec_Battery1_soc"
      unit_of_measurement: "%"
      
    - state_topic: "ingmarsretro/pylontec/1/charging"
      name: "Pylontec_Battery1_charging"
      
    - state_topic: "ingmarsretro/pylontec/1/discharging"
      name: "Pylontec_Battery1_discharging"
     
    - state_topic: "ingmarsretro/pylontec/1/idle"
      name: "Pylontec_Battery1_idle"
      
    - state_topic: "ingmarsretro/pylontec/1/state"
      name: "Pylontec_Battery1_state"
      
    - state_topic: "ingmarsretro/pylontec/1/temp"
      name: "Pylontec_Battery1_temp"
      unit_of_measurement: "°C"
      

On the Homeassistant website, the visualization could then look like this, for example:

Last but not least, I am posting the customized code below. The libraries required for compilation and further information can be found in the GitHub links above.

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <ESP8266WebServer.h>
#include <circular_log.h>
#include <ArduinoJson.h>
#include <NTPClient.h>
#include <ESP8266TimerInterrupt.h>

//+++ START CONFIGURATION +++

//IMPORTANT: Specify your WIFI settings:
#define WIFI_SSID "wifiname"
#define WIFI_PASS deinpasswort1234"
#define WIFI_HOSTNAME "mppsolar-pylontec"

//Uncomment for static ip configuration
#define STATIC_IP
  IPAddress local_IP(192, 168, xxx, yyy);
  IPAddress subnet(255, 255, 255, 0);
  IPAddress gateway(192, 168, xxx, zzz);
  IPAddress primaryDNS(192, 168, xxx, zzz);

//Uncomment for authentication page
//#define AUTHENTICATION
//set http Authentication
const char* www_username = "admin";
const char* www_password = "password";


//IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):
#define ENABLE_MQTT

// Set offset time in seconds to adjust for your timezone, for example:
// GMT +1 = 3600
// GMT +1 = 7200
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
#define GMT 3600

//NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.
//NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc"
#define MQTT_SERVER        "192.168.xx.broker"
#define MQTT_PORT          1883
#define MQTT_USER          ""
#define MQTT_PASSWORD      ""
#define MQTT_TOPIC_ROOT    "ingmarsretro/pylontec/"  //this is where mqtt data will be pushed
#define MQTT_PUSH_FREQ_SEC 2  //maximum mqtt update frequency in seconds

//+++   END CONFIGURATION +++

#ifdef ENABLE_MQTT
#include <PubSubClient.h>
WiFiClient espClient;
PubSubClient mqttClient(espClient);
#endif //ENABLE_MQTT

//text response
char g_szRecvBuff[7000];

const long utcOffsetInSeconds = GMT;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);


ESP8266WebServer server(80);
circular_log<7000> g_log;
bool ntpTimeReceived = false;
int g_baudRate = 0;

void Log(const char* msg)
{
  g_log.Log(msg);
}

//Define Interrupt Timer to Calculate Power meter every second (kWh)
#define USING_TIM_DIV1 true                                             // for shortest and most accurate timer
ESP8266Timer ITimer;
bool setInterval(unsigned long interval, timer_callback callback);      // interval (in microseconds)
#define TIMER_INTERVAL_MS 1000

//Global Variables for the Power Meter - accessible from the calculating interrupt und from main
unsigned long powerIN = 0;       //WS gone in to the BAttery
unsigned long powerOUT = 0;      //WS gone out of the Battery
//Global Variables for the Power Meter - Überlauf
unsigned long powerINWh = 0;       //WS gone in to the BAttery
unsigned long powerOUTWh = 0;      //WS gone out of the Battery

void setup() {
  
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); //clean variable
  
  pinMode(LED_BUILTIN, OUTPUT); 
  digitalWrite(LED_BUILTIN, HIGH);//high is off
  
  // put your setup code here, to run once:
  WiFi.mode(WIFI_STA);
  WiFi.persistent(false); //our credentialss are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)
  WiFi.hostname(WIFI_HOSTNAME);
  #ifdef STATIC_IP
     WiFi.config(local_IP, gateway, subnet, primaryDNS);
  #endif
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  for(int ix=0; ix<10; ix++)
  {
    Log("Wait for WIFI Connection");
    if(WiFi.status() == WL_CONNECTED)
    {
      break;
    }

    delay(1000);
  }

  ArduinoOTA.setHostname(WIFI_HOSTNAME);
  ArduinoOTA.begin();
  server.on("/", handleRoot);
  server.on("/log", handleLog);
  server.on("/req", handleReq);
  server.on("/jsonOut", handleJsonOut);
  server.on("/reboot", [](){
    #ifdef AUTHENTICATION
    if (!server.authenticate(www_username, www_password)) {
      return server.requestAuthentication();
    }
    #endif
    ESP.restart();
  });
  
  server.begin(); 
  
  timeClient.begin();

  
#ifdef ENABLE_MQTT
  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
#endif

  Log("Boot event");
  
}

void handleLog()
{
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  } 
  #endif
  server.send(200, "text/html", g_log.c_str());
}

void switchBaud(int newRate)
{
  if(g_baudRate == newRate)
  {
    return;
  }
  
  if(g_baudRate != 0)
  {
    Serial.flush();
    delay(20);
    Serial.end();
    delay(20);
  }

  char szMsg[50];
  snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate);
  Log(szMsg);
  
  Serial.begin(newRate);
  g_baudRate = newRate;

  delay(20);
}

void waitForSerial()
{
  for(int ix=0; ix<150;ix++)
  {
    if(Serial.available()) break;
    delay(10);
  }
}

int readFromSerial()
{
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));
  int recvBuffLen = 0;
  bool foundTerminator = true;
  
  waitForSerial();
  
  while(Serial.available())
  {
    char szResponse[256] = "";
    const int readNow = Serial.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end)
    if(readNow > 0 && 
       szResponse[0] != '\0')
    {
      if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff)))
      {
        Log("WARNING: Read too much data on the console!");
        break;
      }
      
      strcat(g_szRecvBuff, szResponse);
      recvBuffLen += readNow;

      if(strstr(g_szRecvBuff, "$$\r\n\rpylon"))
      {
        strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add
        foundTerminator = true;
        break; //found end of the string
      }

      if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit"))
      {
        //we need to send new line character so battery continues the output
        Serial.write("\r");
      }

      waitForSerial();
    }
  }

  if(recvBuffLen > 0 )
  {
    if(foundTerminator == false)
    {
      Log("Failed to find pylon> terminator");
    }
  }

  return recvBuffLen;
}

bool readFromSerialAndSendResponse()
{
  const int recvBuffLen = readFromSerial();
  if(recvBuffLen > 0)
  {
    server.sendContent(g_szRecvBuff);
    return true;
  }

  return false;
}

bool sendCommandAndReadSerialResponse(const char* pszCommand)
{
  switchBaud(115200);

  if(pszCommand[0] != '\0')
  {
    Serial.write(pszCommand);
  }
  Serial.write("\n");

  const int recvBuffLen = readFromSerial();
  if(recvBuffLen > 0)
  {
    return true;
  }

  //wake up console and try again:
  wakeUpConsole();

  if(pszCommand[0] != '\0')
  {
    Serial.write(pszCommand);
  }
  Serial.write("\n");

  return readFromSerial() > 0;
}

void handleReq()
{
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  #endif
  bool respOK;
  if(server.hasArg("code") == false)
  {
    respOK = sendCommandAndReadSerialResponse("");
  }
  else
  {
    respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str());
  }

  handleRoot();
}



void handleJsonOut()
{
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  #endif
  if(sendCommandAndReadSerialResponse("pwr") == false)
  {
    server.send(500, "text/plain", "Failed to get response to 'pwr' command");
    return;
  }

  parsePwrResponse(g_szRecvBuff);
  prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));
  server.send(200, "application/json", g_szRecvBuff);
}

void handleRoot() {
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  #endif
  timeClient.update(); //get ntp datetime
  unsigned long days = 0, hours = 0, minutes = 0;
  unsigned long val = os_getCurrentTimeSec();
  days = val / (3600*24);
  val -= days * (3600*24);
  hours = val / 3600;
  val -= hours * 3600;
  minutes = val / 60;
  val -= minutes*60;

  time_t epochTime = timeClient.getEpochTime();
  String formattedTime = timeClient.getFormattedTime();
  //Get a time structure
  struct tm *ptm = gmtime ((time_t *)&epochTime); 
  int currentMonth = ptm->tm_mon+1;

  static char szTmp[9500] = "";  
  long timezone= GMT / 3600;
  snprintf(szTmp, sizeof(szTmp)-1, "<html><b>Pylontech Battery</b><br>Time GMT: %s (%s %d)<br>Uptime: %02d:%02d:%02d.%02d<br><br>free heap: %u<br>Wifi RSSI: %d<BR>Wifi SSID: %s", 
            formattedTime, "GMT ", timezone,
            (int)days, (int)hours, (int)minutes, (int)val, 
            ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str());

  strncat(szTmp, "<BR><a href='/log'>Runtime log</a><HR>", sizeof(szTmp)-1);
  strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'> <a href='/req?code=pwr'>PWR</a> | <a href='/req?code=pwr%201'>Power 1</a> |  <a href='/req?code=pwr%202'>Power 2</a> | <a href='/req?code=pwr%203'>Power 3</a> | <a href='/req?code=pwr%204'>Power 4</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);
  //strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'><a href='/req?code=pwr'>Power</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);
  strncat(szTmp, "<textarea rows='80' cols='180'>", sizeof(szTmp)-1);
  //strncat(szTmp, "<textarea rows='45' cols='180'>", sizeof(szTmp)-1);
  strncat(szTmp, g_szRecvBuff, sizeof(szTmp)-1);
  strncat(szTmp, "</textarea></form>", sizeof(szTmp)-1);
  strncat(szTmp, "</html>", sizeof(szTmp)-1);
  //send page
  server.send(200, "text/html", szTmp);
}

unsigned long os_getCurrentTimeSec()
{
  static unsigned int wrapCnt = 0;
  static unsigned long lastVal = 0;
  unsigned long currentVal = millis();

  if(currentVal < lastVal)
  {
    wrapCnt++;
  }

  lastVal = currentVal;
  unsigned long seconds = currentVal/1000;
  
  //millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter
  return (wrapCnt*4294967) + seconds;
}

void wakeUpConsole()
{
  switchBaud(1200);

  //byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};
  //Serial.write(wakeUpBuff, sizeof(wakeUpBuff));
  Serial.write("~20014682C0048520FCC3\r");
  delay(1000);

  byte newLineBuff[] = {0x0E, 0x0A};
  switchBaud(115200);
  
  for(int ix=0; ix<10; ix++)
  {
    Serial.write(newLineBuff, sizeof(newLineBuff));
    delay(1000);

    if(Serial.available())
    {
      while(Serial.available())
      {
        Serial.read();
      }
      
      break;
    }
  }
}

#define MAX_PYLON_BATTERIES 8

struct pylonBattery
{
  bool isPresent;
  long  soc;     //Coulomb in %
  long  voltage; //in mW
  long  current; //in mA, negative value is discharge
  long  tempr;   //temp of case or BMS?
  long  cellTempLow;
  long  cellTempHigh;
  long  cellVoltLow;
  long  cellVoltHigh;
  char baseState[9];    //Charge | Dischg | Idle
  char voltageState[9]; //Normal
  char currentState[9]; //Normal
  char tempState[9];    //Normal
  char time[20];        //2019-06-08 04:00:29
  char b_v_st[9];       //Normal  (battery voltage?)
  char b_t_st[9];       //Normal  (battery temperature?)

  bool isCharging()    const { return strcmp(baseState, "Charge")   == 0; }
  bool isDischarging() const { return strcmp(baseState, "Dischg")   == 0; }
  bool isIdle()        const { return strcmp(baseState, "Idle")     == 0; }
  bool isBalancing()   const { return strcmp(baseState, "Balance")  == 0; }
  

  bool isNormal() const
  {
    if(isCharging()    == false &&
       isDischarging() == false &&
       isIdle()        == false &&
       isBalancing()   == false)
    {
      return false; //base state looks wrong!
    }

    return  strcmp(voltageState, "Normal") == 0 &&
            strcmp(currentState, "Normal") == 0 &&
            strcmp(tempState,    "Normal") == 0 &&
            strcmp(b_v_st,       "Normal") == 0 &&
            strcmp(b_t_st,       "Normal") == 0 ;
  }
};

struct batteryStack
{
  int batteryCount;
  int soc;  //in %, if charging: average SOC, otherwise: lowest SOC
  int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest
  long currentDC;    //mAh current going in or out of the battery
  long avgVoltage;    //in mV
  char baseState[9];  //Charge | Dischg | Idle | Balance | Alarm!

  
  pylonBattery batts[MAX_PYLON_BATTERIES];

  bool isNormal() const
  {
    for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
    {
      if(batts[ix].isPresent && 
         batts[ix].isNormal() == false)
      {
        return false;
      }
    }

    return true;
  }

  //in Wh
  long getPowerDC() const
  {
    return (long)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
  }

  // power in Wh in charge
  float powerIN() const
  {
    if (currentDC > 0) {
       return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
    } else {
       return (float)(0);
    }
  }
  
  // power in Wh in discharge
  float powerOUT() const
  {
    if (currentDC < 0) {
       return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0)*-1);
    } else {
       return (float)(0);
    }
  }

  //Wh estimated current on AC side (taking into account Sofar ME3000SP losses)
  long getEstPowerAc() const
  {
    double powerDC = (double)getPowerDC();
    if(powerDC == 0)
    {
      return 0;
    }
    else if(powerDC < 0)
    {
      //we are discharging, on AC side we will see less power due to losses
      if(powerDC < -1000)
      {
        return (long)(powerDC*0.94);
      }
      else if(powerDC < -600)
      {
        return (long)(powerDC*0.90);
      }
      else
      {
        return (long)(powerDC*0.87);
      }
    }
    else
    {
      //we are charging, on AC side we will have more power due to losses
      if(powerDC > 1000)
      {
        return (long)(powerDC*1.06);
      }
      else if(powerDC > 600)
      {
        return (long)(powerDC*1.1);
      }
      else
      {
        return (long)(powerDC*1.13);
      }
    }
  }
};

batteryStack g_stack;


long extractInt(const char* pStr, int pos)
{
  return atol(pStr+pos);
}

void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)
{
  strOut[strOutSize-1] = '\0';
  strncpy(strOut, pStr+pos, strOutSize-1);
  strOutSize--;
  
  
  //trim right
  while(strOutSize > 0)
  {
    if(isspace(strOut[strOutSize-1]))
    {
      strOut[strOutSize-1] = '\0';
    }
    else
    {
      break;
    }

    strOutSize--;
  }
}

/* Output has mixed \r and \r\n
pwr

@

Power Volt   Curr   Tempr  Tlow   Thigh  Vlow   Vhigh  Base.St  Volt.St  Curr.St  Temp.St  Coulomb  Time                 B.V.St   B.T.St  

1     49735  -1440  22000  19000  19000  3315   3317   Dischg   Normal   Normal   Normal   93%      2019-06-08 04:00:30  Normal   Normal  

....   

8     -      -      -      -      -      -      -      Absent   -        -        -        -        -                    -        -       

Command completed successfully

$$

pylon
*/
bool parsePwrResponse(const char* pStr)
{
  if(strstr(pStr, "Command completed successfully") == NULL)
  {
    return false;
  }
  
  int chargeCnt    = 0;
  int dischargeCnt = 0;
  int idleCnt      = 0;
  int alarmCnt     = 0;
  int socAvg       = 0;
  int socLow       = 0;
  int tempHigh     = 0;
  int tempLow      = 0;

  memset(&g_stack, 0, sizeof(g_stack));

  for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
  {
    char szToFind[32] = "";
    snprintf(szToFind, sizeof(szToFind)-1, "\r\r\n%d     ", ix+1);

    const char* pLineStart = strstr(pStr, szToFind);
    if(pLineStart == NULL)
    {
      return false;
    }

    pLineStart += 3; //move past \r\r\n

    extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));
    if(strcmp(g_stack.batts[ix].baseState, "Absent") == 0)
    {
      g_stack.batts[ix].isPresent = false;
    }
    else
    {
      g_stack.batts[ix].isPresent = true;
      extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));
      extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));
      extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));
      extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));
      extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));
      extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));
      g_stack.batts[ix].voltage = extractInt(pLineStart, 6);
      g_stack.batts[ix].current = extractInt(pLineStart, 13);
      g_stack.batts[ix].tempr   = extractInt(pLineStart, 20);
      g_stack.batts[ix].cellTempLow    = extractInt(pLineStart, 27);
      g_stack.batts[ix].cellTempHigh   = extractInt(pLineStart, 34);
      g_stack.batts[ix].cellVoltLow    = extractInt(pLineStart, 41);
      g_stack.batts[ix].cellVoltHigh   = extractInt(pLineStart, 48);
      g_stack.batts[ix].soc            = extractInt(pLineStart, 91);

      //////////////////////////////// Post-process ////////////////////////
      g_stack.batteryCount++;
      g_stack.currentDC += g_stack.batts[ix].current;
      g_stack.avgVoltage += g_stack.batts[ix].voltage;
      socAvg += g_stack.batts[ix].soc;

      if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }
      else if(g_stack.batts[ix].isCharging()){chargeCnt++;}
      else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}
      else if(g_stack.batts[ix].isIdle()){idleCnt++;}
      else{ alarmCnt++; } //should not really happen!

      if(g_stack.batteryCount == 1)
      {
        socLow = g_stack.batts[ix].soc;
        tempLow  = g_stack.batts[ix].cellTempLow;
        tempHigh = g_stack.batts[ix].cellTempHigh;
      }
      else
      {
        if(socLow > g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}
        if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}
        if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}
      }
      
    }
  }

  //now update stack state:
  g_stack.avgVoltage /= g_stack.batteryCount;
  g_stack.soc = socLow;

  if(tempHigh > 15000) //15C
  {
    g_stack.temp = tempHigh; //in the summer we highlight the warmest cell
  }
  else
  {
    g_stack.temp = tempLow; //in the winter we focus on coldest cell
  }

  if(alarmCnt > 0)
  {
    strcpy(g_stack.baseState, "Alarm!");
  }
  else if(chargeCnt == g_stack.batteryCount)
  {
    strcpy(g_stack.baseState, "Charge");
    g_stack.soc = (int)(socAvg / g_stack.batteryCount);
  }
  else if(dischargeCnt == g_stack.batteryCount)
  {
    strcpy(g_stack.baseState, "Dischg");
  }
  else if(idleCnt == g_stack.batteryCount)
  {
    strcpy(g_stack.baseState, "Idle");
  }
  else
  {
    strcpy(g_stack.baseState, "Balance");
  }


  return true;
}

void prepareJsonOutput(char* pBuff, int buffSize)
{
  memset(pBuff, 0, buffSize);
  snprintf(pBuff, buffSize-1, "{\"soc\": %d, \"temp\": %d, \"currentDC\": %ld, \"avgVoltage\": %ld, \"baseState\": \"%s\", \"batteryCount\": %d, \"powerDC\": %ld, \"estPowerAC\": %ld, \"isNormal\": %s}", g_stack.soc, 
                                                                                                                                                                                                            g_stack.temp, 
                                                                                                                                                                                                            g_stack.currentDC, 
                                                                                                                                                                                                            g_stack.avgVoltage, 
                                                                                                                                                                                                            g_stack.baseState, 
                                                                                                                                                                                                            g_stack.batteryCount, 
                                                                                                                                                                                                            g_stack.getPowerDC(), 
                                                                                                                                                                                                            g_stack.getEstPowerAc(),
                                                                                                                                                                                                            g_stack.isNormal() ? "true" : "false");
}

void loop() {
#ifdef ENABLE_MQTT
  mqttLoop();
#endif
  
  ArduinoOTA.handle();
  server.handleClient();

  //if there are bytes availbe on serial here - it's unexpected
  //when we send a command to battery, we read whole response
  //if we get anything here anyways - we will log it
  int bytesAv = Serial.available();
  if(bytesAv > 0)
  {
    if(bytesAv > 63)
    {
      bytesAv = 63;
    }
    
    char buff[64+4] = "RCV:";
    if(Serial.readBytes(buff+4, bytesAv) > 0)
    {
      digitalWrite(LED_BUILTIN, LOW);
      delay(5);
      digitalWrite(LED_BUILTIN, HIGH);//high is off

      Log(buff);
    }
  }
}

#ifdef ENABLE_MQTT
#define ABS_DIFF(a, b) (a > b ? a-b : b-a)
void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)
{
  char szTmp[16] = "";
  snprintf(szTmp, 15, "%.2f", newValue);
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)
  {
    mqttClient.publish(topic, szTmp, false);
  }
}

void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)
{
  char szTmp[16] = "";
  snprintf(szTmp, 15, "%d", newValue);
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)
  {
    mqttClient.publish(topic, szTmp, false);
  }
}

void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)
{
  if(force || strcmp(newValue, oldValue) != 0)
  {
    mqttClient.publish(topic, newValue, false);
  }
}

void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */)
{
  mqtt_publish_f(MQTT_TOPIC_ROOT "soc",          g_stack.soc,                lastSentData.soc,                0, forceUpdate);
  mqtt_publish_f(MQTT_TOPIC_ROOT "temp",         (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0.1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "currentDC",    g_stack.currentDC,          lastSentData.currentDC,          1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC",   g_stack.getEstPowerAc(),    lastSentData.getEstPowerAc(),   10, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount,       lastSentData.batteryCount,       0, forceUpdate);
  mqtt_publish_s(MQTT_TOPIC_ROOT "base_state",   g_stack.baseState,          lastSentData.baseState            , forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal",    g_stack.isNormal() ? 1:0,   lastSentData.isNormal() ? 1:0,   0, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "getPowerDC",   g_stack.getPowerDC(),       lastSentData.getPowerDC(),       1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerIN",      g_stack.powerIN(),          lastSentData.powerIN(),          1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerOUT",     g_stack.powerOUT(),         lastSentData.powerOUT(),         1, forceUpdate);

  // publishing details
  for (int ix = 0; ix < g_stack.batteryCount; ix++) {
    char ixBuff[50];
    String ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/voltage";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_f(ixBuff, g_stack.batts[ix].voltage / 1000.0, lastSentData.batts[ix].voltage / 1000.0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/current";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_f(ixBuff, g_stack.batts[ix].current / 1000.0, lastSentData.batts[ix].current / 1000.0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/soc";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].soc, lastSentData.batts[ix].soc, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/charging";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].isCharging()?1:0, lastSentData.batts[ix].isCharging()?1:0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/discharging";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].isDischarging()?1:0, lastSentData.batts[ix].isDischarging()?1:0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/idle";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].isIdle()?1:0, lastSentData.batts[ix].isIdle()?1:0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/state";
    ixBattStr.toCharArray(ixBuff, 50);
    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);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/temp";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_f(ixBuff, (float)g_stack.batts[ix].tempr/1000.0, (float)lastSentData.batts[ix].tempr/1000.0, 0.1, forceUpdate);
  }
} 

void mqttLoop()
{
  //if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)
  static unsigned long g_lastConnectionAttempt = 0;

  //first: let's make sure we are connected to mqtt
  const char* topicLastWill = MQTT_TOPIC_ROOT "availability";
  if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) {
    if(mqttClient.connect(WIFI_HOSTNAME, MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline"))
    {
      Log("Connected to MQTT server: " MQTT_SERVER);
      mqttClient.publish(topicLastWill, "online", true);
    }
    else
    {
      Log("Failed to connect to MQTT server.");
    }

    g_lastConnectionAttempt = os_getCurrentTimeSec();
  }

  //next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)
  static unsigned long g_lastDataSent = 0;
  if(mqttClient.connected() && 
     os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC &&
     sendCommandAndReadSerialResponse("pwr") == true)
  {
    static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again
    static unsigned int callCnt = 0;
    
    parsePwrResponse(g_szRecvBuff);

    bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call
    pushBatteryDataToMqtt(lastSentData, forceUpdate);
    
    callCnt++;
    g_lastDataSent = os_getCurrentTimeSec();
    memcpy(&lastSentData, &g_stack, sizeof(batteryStack));
  }
  
  mqttClient.loop();
}

#endif //ENABLE_MQTT

 

Read EVU smart meters with ESP32 and ESPhome and use them in Homeassistant

Loading

edit 7.11.24
In the meantime, I have also layouted an interface board with a USB type B socket for the 5V supply. (see layout below). Because as small and fine as the micro USB plugs are, I need something more robust.

new board version with USB type B socket for power supply

As I am asked more and more often for the production data, I am making the Gerber data of the circuit boards available for download:

ESP32_interface_2023-05-12

interface_usbB_2024-06-27


In the article entitled: “Reading energy supply company smart meters with ESP32 and sending data via MQTT” (link), I described how the energy supply companies’ smart meters can be read out via the customer interface. The measurement data is then available as topics via the mqtt broker and can be further processed in various home automation systems (HomeMatic, Homeassistant, etc.). All you need is an ESP32 board and a few small parts to establish the connection to the smart meter. As a small update, I have now embellished the structure (back then with pin headers on a breadboard) a little and made a circuit board.

Layout preview in designtool

The associated circuit diagram essentially corresponds to the sketch in the previous article. To make things a little more convenient with the new circuit board, the connection to the customer interface of the smart meter can be plugged in via an RJ socket. I have also implemented the power supply via a USB socket.

Once the ESP32 circuit board had been fitted and plugged in, the device was given a small housing and is now doing its job in the electrical distribution cabinet.

The hardware is therefore ready and functional. I have also considered changing something about the software. Until now, the ESP was running a program that decrypted the data from the smart meter and then sent it to the IP address of the broker via MQTT. However, as I am now also a user of the ESPHome integration in my HomeAssistant environment, I have flashed the ESP with an ESPHome base image. On GitHub there is the repository of Andre-Schuiki, where he publishes a version for ISKRA and SIEMENS Smartmeter for use with ESPHome. The installation instructions can be found under the following link: https://github.com/Andre-Schuiki/esphome_im350/tree/main/esp_home

The script for the ESPHome device looks like this:

 esphome:  
  name: kelagsmartmeter  
  friendly_name: KelagSmartmeter  
  libraries:  
  - "Crypto" # !IMPORTANT! we need this library for decryption!  
 esp32:  
  board: esp32dev  
  framework:  
   type: arduino  
 # Enable logging  
 logger:  
 # Enable Home Assistant API  
 api:  
  encryption:  
   key: "da kommt der key rein des neu angelegten ESPHome Gerätes rein"  
 ota:  
  password: "das automatisch generierte ota passwort"  
 wifi:  
  ssid: !secret wifi_ssid  
  password: !secret wifi_password  
  # Enable fallback hotspot (captive portal) in case wifi connection fails  
  ap:  
   ssid: "Kelagsmartmeter Fallback Hotspot"  
   password: "das automatisch generierte password"  
 captive_portal:  
 external_components:  
  - source:  
    type: local  
    path: custom_esphome  
 sensor:  
  - platform: siemens_im350  
   update_interval: 5s  
   trigger_pin: 26 # this pin goes to pin 2 of the customer interface and will be set to high before we try to read the data from the rx pin  
   rx_pin: 16 # this pin goes to pin 5 of the customer interface  
   tx_pin: 17 # not connected at the moment, i added it just in case we need it in the future..  
   decryption_key: "00AA01BB02CC03DD04EE05FF06AA07BB" # you get the key from your provider!  
   use_test_data: false # that was just for debugging, if you set it to true data are not read from serial and the test_data string is used  
   test_data: "7EA077CF022313BB45E6E700DB0849534B697460B6FA5F200005C8606F536D06C32A190761E80A97E895CECA358D0A0EFD7E9C47A005C0F65B810D37FB0DA2AD6AB95F7F372F2AB11560E2971B914A5F8BFF5E06D3AEFBCD95B244A373C5DBDA78592ED2C1731488D50C0EC295E9056B306F4394CDA7D0FC7E0000"  
   delay_before_reading_data: 1000 # this is needed because we have to wait for the interface to power up, you can try to lower this value but 1 sec was ok for me  
   max_wait_time_for_reading_data: 1100 # maximum time to read the 123 Bytes (just in case we get no data)  
   ntp_server: "pool.ntp.org" #if no ntp is specified pool.ntp.org is used  
   ntp_gmt_offset: 3600  
   ntp_daylight_offset: 3600  
   counter_reading_p_in:  
    name: reading_p_in  
    filters:  
     - lambda: return x / 1000;  
    unit_of_measurement: kWh  
    accuracy_decimals: 3  
    device_class: energy  
   counter_reading_p_out:  
    name: reading_p_out  
    filters:  
     - lambda: return x / 1000;  
    unit_of_measurement: kWh  
    accuracy_decimals: 3  
    device_class: energy  
   counter_reading_q_in:  
    name: reading_q_in  
    filters:  
     - lambda: return x / 1000;  
    unit_of_measurement: kvarh  
    device_class: energy  
   counter_reading_q_out:  
    name: reading_q_out  
    filters:  
     - lambda: return x / 1000;  
    unit_of_measurement: kvarh  
    device_class: energy  
   current_power_usage_in:  
    name: power_usage_in  
    filters:  
     - lambda: return x / 1000;  
    unit_of_measurement: kW  
    accuracy_decimals: 3  
    device_class: energy  
   current_power_usage_out:  
    name: power_usage_out  
    filters:  
     - lambda: return x / 1000;  
    unit_of_measurement: kW  
    accuracy_decimals: 3  
    device_class: energy  
  # Extra sensor to keep track of uptime  
  - platform: uptime  
   name: IM350_Uptime Sensor  
 switch:  
  - platform: restart  
   name: IM350_Restart  

 

“Tricky Traps” restoration

Loading

In this post, I’m going to take a look at the restoration – or rather repair – of a handheld game that I recently received for review. It has the name “Tricky Traps”. This means something like “tricky or tricky traps”

The game “Tricky Traps” by Tomy is a mechanical game of skill that was originally released in the 1970s. It consists of a maze-like playing field in which the player must navigate a small metal ball through a series of obstacles and traps. The aim of the game is to successfully maneuver the ball through the maze to the finish without it falling into one of the many traps. There are five balls available. The game is timed.

Tricky Traps

Game mechanics:

  • The player starts the game with a rotary knob, which sets a small electric motor in motion. This motor drives the “traps” and also the rotary knob itself via a gearbox. This is how the “timer” is realized. Once the rotary knob has completed about three quarters of a turn, the motor stops again and the game is over. This is solved by a contact spring, which is pressed down onto a mating contact by a small bar at the bottom of the rotary knob.
  • Once the game has started, the red button can be used to release a ball into the track. The white button at the bottom is the actual and only game button. It lifts the ball with a small cylinder so that it can move through the various parts of the playing field. You have to do this with the right timing.
  • There are numerous obstacles on the playing field, such as rotating disks, small ramps, narrow passages and a rotating magnet that can stop the ball or cause it to fall into a trap.

The colorful design is typical of the mechanical games of the 70s and 80s. It is made of plastic and the moving parts are usually brightly colored.

The technical problems that occur often in such old games are:

  • leaking batteries, which usually cause corrosion and destruction of the contacts
  • Brittle plastic, which mainly occurs with gearwheels that are mounted on brass shafts and therefore start to slip. This also means that housing gears often no longer hold together properly.
  • Electric motors whose brushes are worn so that they no longer turn
  • Resinous grease and oils that make moving parts sluggish
  • Wires and electrical connections that are corroded and broken

All of these points can be found very often during restoration and must be fixed. This can also be done more or less easily. After carefully opening and inspecting the appliance, I actually start by completely dismantling and cleaning the parts. Then I try to repair any broken plastic parts. Here I use various adhesives as far as possible. Sometimes it is also necessary to reproduce a part with a 3D printer. Of course, this assumes that enough of the original part is still available to reconstruct it accurately. The electrical components of these devices are the easiest to repair, as there are usually no electronics with any components with ICs that are no longer manufactured.

Revealing the parts after disassembly

Gearbox
Assembly after cleaning

 

“Ball” button to start the ball