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)
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.