Emonhub interfacer for Pylontech BMS?

Has anyone written an emonhub interfacer for getting data from the Pylontech BMS and logging to EmonCms?

I’ve written a standalone python script that reads data from the Pylontech console port and posts to emoncms.org, using a special console to USB cable supplied by Servtec. This currently reports currant, voltage, SoC, min/max cell voltage and temperatures for each module.

I’m considering extending this into a proper emonhub interfacer, but has anyone already done this?

I’ve been thinking about trying to collect data from our Pylontech battery system, and was looking to perhaps use the WiFi interface. The console solution sounds good and could hopefully be added to the local Pi that is already sending CT and voltage data from the solar installation in the garage to the local emonCMS system in the house.

Perhaps you could share details of the console cable and the python script that you have written?
(We have a Pylontech Force L2 battery system)

The console cable is one of these:

When connected to my EmonPi2, it appears as a serial port /dev/ttyUSB0.

Here’s my python script. Don’t judge it - I’m no python expert.

It simply sends a ‘pwr’ command to the Pylontech console port every 10 seconds, parses the result, and posts to emoncms.org as a json data string. The result contains 1 line for each pylontech module with voltage, current, SoC, some temperatures, and min/max cell voltrages and some statuses which I‘m not bothering to process.

import serial
import time
import requests


port = serial.Serial("/dev/ttyUSB0", baudrate=115200, timeout=3.0)
url="https://emoncms.org/input/post?node=pylontech&apikey=enter_your_api_key_here"

  
class Battery:
    Module = 0
    Voltage = 0
    Current = 0
    Temp = 0
    MinCellTemp = 0
    MaxCellTemp = 0
    MinCellVoltage = 0
    MaxCellVoltage = 0
    SoC = 0
    
#create array of 16 battery objects
Batteries = [] #empty array
  
def processResponse( resp ):
    # A good response will look something like this:
    #Power Volt   Curr   Tempr  Tlow   Thigh  Vlow   Vhigh  Base.St  Volt.St  Curr.St  Temp.St  Coulomb  Time                 B.V.St   B.T.St   MosTempr M.T.St  
    #1   49074  -3596  23600  20900  21300  3271   3272   Dischg   Normal   Normal   Normal   59%      2025-10-19 23:36:03  Normal   Normal  22000    Normal  
    #2   49081  -3561  24400  22000  22000  3272   3273   Dischg   Normal   Normal   Normal   57%      2025-10-19 23:36:03  Normal   Normal  23500    Normal  
    #Followed by additionbal lines for up to each of 16 battery packs.
    #The final two values (MosTempr and M.T.St are not present for US2000 packs are appear as '-' 
    #
    #Due to a tendency of the serial data to drop characters, we need to sanity check carefully that each value is of the correct length before processing it.
    
    # Split into lines:
    lines = resp.splitlines()
    global Batteries 
    global url
    Batteries = []
    # process each lines
    for line in lines:
        processLine(line)
        
    # build json string to send
    totalCurrent = 0
    params = "{"

    for battery in Batteries:
        number = battery.Module
        current = battery.Current
        totalCurrent += current
        params += 'Battery' + str(number) + '_Current'  ":" + str(current) + ","
        
        # these get averaged across all 15 cells 
        ballance_mV = (battery.MaxCellVoltage - battery.MinCellVoltage)

        params += 'Battery' + str(number) + '_maxCellVolts'  ":" + str(battery.MaxCellVoltage/1000.0) + ","
        params += 'Battery' + str(number) + '_minCellVolts'  ":" + str(battery.MinCellVoltage/1000.0) + ","
        params += 'Battery' + str(number) + '_maxCellTemp'   ":" + str(battery.MaxCellTemp) + ","
        params += 'Battery' + str(number) + '_minCellTemp'  ":" + str(battery.MinCellTemp) + ","

        params += 'Battery' + str(number) + '_SoC'  ":" + str(battery.SoC) + ","
        params += 'Battery' + str(number) + '_BalmV' ":" + str(ballance_mV) + ","
        params += 'Battery' + str(number) + '_Volts' ":" + str(battery.Voltage) + ","
       
        
    # Add totals
    params += 'TotalCurrent'+ ":" + str(totalCurrent )
    params += "}"

    localurl = url
    localurl += "&json=" + params

    print(localurl)

    # now open the url and send the values
    response = requests.post(localurl, params)

    # read the response and print it
    str_response = response.content.decode("utf-8")
    print(response.content)

   
        
def processLine(line):
    global Batteries 
    #print ("Processing line:" + line)
    try:
        # split line into fields
        fields = line.split() #Note: the Time/Date will be split as two seperate fields
        
        if len(fields) != 19: return;
        
        # sanity check each field in turn
        # field 0: module number integer 1 to 16
        field = fields[0]
        value = int(field)
        if value < 1 or value > 16: return

        bat = Battery()
        bat.Module = value
        
        
        # Field 1: Voltage in mV. Will always be around 48-50 V.
        field = fields[1]
        value = int(field)
        if value < 45000 or value > 55000: return
            
        bat.Voltage = value/1000.0
        
        #Field 2: current in mA. Not sure what the value range for this is
        field = fields[2]
        value = int(field)
        if value < -50000 or value > 50000: return
            
        bat.Current = value / 1000.0
        
        #Field 3: Temperature: always 5 digits between around 10000 and 30000
        field = fields[3]
        if len(field) != 5: return
        
        value = int(field)
        if value < 10000 or value > 50000: return
            
        bat.Temp = value / 1000.0
        
        #Field 4: Min cell Temperature: always 5 digits between around 10000 and 30000
        field = fields[4]
        if len(field) != 5: return
        
        value = int(field)
        if value < 10000 or value > 50000: return
            
        bat.MinCellTemp = value / 1000.0
        
        #Field 5: Max cell Temperature: always 5 digits between around 10000 and 30000
        field = fields[5]
        if len(field) != 5: return
        
        value = int(field)
        if value < 10000 or value > 50000: return
            
        bat.MaxCellTemp = value / 1000.0
        
        # fields 6 and 7 are the min and max cell voltages - 4 characters and will be between 3000 and 4000 mV
        field = fields[6]
        if len(field) != 4: return
        
        value = int(field)
        if value < 3000 or value > 4000: return
            
        bat.MinCellVoltage = value

        field = fields[7]
        if len(field) != 4: return
        
        value = int(field)
        if value < 3000 or value > 4000: return
            
        bat.MaxCellVoltage = value
        
        # finally, field 12 is the SoC percentage.
        field = fields[12]
        # strip % character
        field = field.replace("%", "")
        value = int(field)
        if value < 0 or value > 120: return
            
        bat.SoC = value
        Batteries.append(bat)
        print (f"Battery: {bat.Module}: {bat.Voltage}V, Current: {bat.Current}A, Temp: {bat.Temp}, MinCell Temp: {bat.MinCellTemp}, MaxCell Temp: {bat.MaxCellTemp}, Min Cell Voltage: {bat.MinCellVoltage}mV, Max Cell Voltage: {bat.MaxCellVoltage}mV, SoC: {bat.SoC}%")
        
    
    except Exception as e:
        # bomb out - invalid data
        print ("Error:", e )
        return
    
while True:
    try:
        time.sleep(10)
        print ("Send command :pwr")
        port.flushInput()
        port.write(b"pwr\n")
        rcv = port.read_until(b"pylon>")
        data = rcv.decode()
        #print (str)
        processResponse(data)

    except Exception as e:
        print ("Error:", e )

Thanks, that’s most helpful. I’ve just checked and I’m not using the USB port on the local Pi Zero, so should be able to sort out something suitable based on this. :slightly_smiling_face: