Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions battery_management_systems/pylontech_us2000_rs485/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Pylontech US200 (RS485)
# Pylontech batteries

This free [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates **Pylontech US2000** lithium battery control and monitoring via the [RS-485 communication interface](https://go.enapter.com/developers-enapter-rs485) into Enapter Cloud. The device can also support the CAN communication interface (configured by vendor).
This free [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates **Pylontech residential batteries** via the [RS-485 communication interface](https://go.enapter.com/developers-enapter-rs485) into Enapter Cloud. The device can also support the CAN communication interface (configured by vendor).

## Connect to Enapter

- Sign up to the Enapter Cloud using the [Web](https://cloud.enapter.com/) or mobile app ([iOS](https://apps.apple.com/app/id1388329910), [Android](https://play.google.com/store/apps/details?id=com.enapter&hl=en)).
- Use the [Enapter ENP-RS485](https://go.enapter.com/handbook-enp-rs485) module as gateway for physical connection of Pylontech US2000. See [connection instructions](https://go.enapter.com/handbook-enp-rs485-conn) in the module manual.
- Use the [Enapter ENP-RS485](https://go.enapter.com/handbook-enp-rs485) module as gateway for physical connection of Pylontech battery. See [connection instructions](https://go.enapter.com/handbook-enp-rs485-conn) in the module manual.
- [Add ENP-RS485 to your site](https://go.enapter.com/handbook-mobile-app) using the mobile app.
- [Upload](https://go.enapter.com/developers-upload-blueprint) this blueprint to ENP-RS485.

## References

- [Pylontech US2000 product page](https://go.enapter.com/pylontech)
- [Pylontech batteries product range](https://go.enapter.com/pylontech)
168 changes: 96 additions & 72 deletions battery_management_systems/pylontech_us2000_rs485/firmware.lua
Original file line number Diff line number Diff line change
@@ -1,95 +1,136 @@
-- Pylontech US2000 Battery
-- Serial over RS485
-- Baud rate = 9600 bps, 8 data bits, no parity, 1 stop bit
local config = require('enapter.ucm.config')

BAUDRATE_CONFIG = 'baudrate'
CELLS_CONFIG = 'cells'
GROUP_CONFIG = 'group'

local BAUDRATE, CELL_COUNT, GROUP_COUNT
local DATA_BITS, PARITY, STOP_BITS = 8, 'N', 1

function main()
local result = rs485.init(9600, 8, "N", 1)
config.init({
[BAUDRATE_CONFIG] = { type = 'number', required = true, default = 115200 },
[CELLS_CONFIG] = { type = 'number', required = false, default = 2 },
[GROUP_CONFIG] = { type = 'number', required = false, default = 0 },
})

local values, err = config.read_all()
if err then
enapter.log('cannot read config: '..tostring(err), 'error')
else
BAUDRATE = math.floor(values[BAUDRATE_CONFIG])
end

local result = rs485.init(BAUDRATE, DATA_BITS, PARITY, STOP_BITS)
if result ~= 0 then
enapter.log("RS485 init failed: "..rs485.err_to_str(result))
end

scheduler.add(30000, properties)
scheduler.add(1000, telemetry)
scheduler.add(30000, send_properties)
scheduler.add(1000, send_telemetry)
end

function properties()
enapter.send_properties({ vendor = "Pylontech", model = "US2000" })
function send_properties()
local values, err = config.read_all()
if err then
enapter.log('cannot read config: '..tostring(err), 'error')
end

enapter.send_properties({
vendor = "Pylontech",
baudrate = math.floor(values[BAUDRATE_CONFIG]),
cells = CELL_COUNT,
groups = GROUP_COUNT
})
end

function telemetry()
local b0_TotalCurrent, b0_TotalVoltage, b0_RemainingCapacity, b0_TotalCapacity = get_analog_value(0)
local b1_TotalCurrent, b1_TotalVoltage, b1_RemainingCapacity, b1_TotalCapacity = get_analog_value(1)
local b2_TotalCurrent, b2_TotalVoltage, b2_RemainingCapacity, b2_TotalCapacity = get_analog_value(2)
local b3_TotalCurrent, b3_TotalVoltage, b3_RemainingCapacity, b3_TotalCapacity = get_analog_value(3)

if b0_TotalCurrent then
enapter.send_telemetry({
b0_total_current = b0_TotalCurrent,
b0_total_voltage = b0_TotalVoltage,
b0_total_power = b0_TotalCurrent * b0_TotalVoltage,
b0_battery_level = b0_RemainingCapacity / b0_TotalCapacity * 100,
b1_total_current = b1_TotalCurrent,
b1_total_voltage = b1_TotalVoltage,
b1_total_power = b1_TotalCurrent * b1_TotalVoltage,
b1_battery_level = b1_RemainingCapacity / b1_TotalCapacity * 100,
b2_total_current = b2_TotalCurrent,
b2_total_voltage = b2_TotalVoltage,
b2_total_power = b2_TotalCurrent * b2_TotalVoltage,
b2_battery_level = b2_RemainingCapacity / b2_TotalCapacity * 100,
b3_total_current = b3_TotalCurrent,
b3_total_voltage = b3_TotalVoltage,
b3_total_power = b3_TotalCurrent * b3_TotalVoltage,
b3_battery_level = b3_RemainingCapacity / b3_TotalCapacity * 100
})
function send_telemetry()
local values, err = config.read_all()
if err then
enapter.log('cannot read config: '..tostring(err), 'error')
else
CELL_COUNT = values[CELLS_CONFIG]
end

local telemetry = {}
for cell = 0, CELL_COUNT do
merge_tables(telemetry, get_analog_value(cell))
end

enapter.send_telemetry(telemetry)
end

function get_analog_value(addr)
local message = make_message(addr, 0x42, 2)

rs485.send(message)
-- enapter.log("Sent message: "..message)

local data, res = rs485.receive(2000)
local response, res = rs485.receive(2000)

if not data then
enapter.log("RS485 receiving failed: "..rs485.err_to_str(res))
return nil
if not response then
enapter.log("RS485 receiving failed (address "..addr.."): "..rs485.err_to_str(res))
return {}
end

local binary_data = fromhex(data:sub(14, -6))
local INFO_START_SYMBOL_COUNT = 15
local binary_data = from_hex_to_char(response:sub(INFO_START_SYMBOL_COUNT, -6))

local CellsCount = binary_data:byte(3)
local TempCount = binary_data:byte(CellsCount * 2 + 4)
local data = {}

-- uncoment for debug and check raw response in hex
-- local s = ''
-- for i = 1, #binary_data do
-- s = s .. string.format("%02X", response:byte(i)) .. ' '
-- end
-- enapter.log('RESPONSE: '..s)

CELL_COUNT = binary_data:byte(2)
GROUP_COUNT = binary_data:byte(CELL_COUNT * 2 + 4)
local data_pos = CELL_COUNT * 2 + GROUP_COUNT * 2 + 9

data['total_current_'..addr] = get_int2_complement(binary_data:sub(data_pos, data_pos + 1)) / 100.0

local data_pos = CellsCount * 2 + TempCount * 2 + 9
local TotalCurrent = get_int2_complement(binary_data:sub(data_pos, data_pos + 1)) / 100.0
data_pos = data_pos + 2
local TotalVoltage = get_int2(binary_data:sub(data_pos, data_pos + 1)) / 100.0
data_pos = data_pos + 2
local RemainingCapacity = get_int2(binary_data:sub(data_pos, data_pos + 1)) / 100.0
data['total_voltage_'..addr] = get_int2(binary_data:sub(data_pos, data_pos + 1)) / 100.0

data['total_power_'..addr] = data['total_voltage_'..addr] * data['total_current_'..addr]

data_pos = data_pos + 2
local TotalCapacity = get_int2(binary_data:sub(data_pos, data_pos + 1)) / 100.0
data['remaining_capacity_'..addr] = get_int2(binary_data:sub(data_pos, data_pos + 1)) / 100.0

data_pos = data_pos + 2
local Cycles = get_int2(binary_data:sub(data_pos, data_pos + 1))
data['total_capacity_'..addr] = get_int2(binary_data:sub(data_pos, data_pos + 1)) / 100.0

data['battery_level_'..addr] = data['remaining_capacity_'..addr] / data['total_capacity_'..addr] * 100

return TotalCurrent, TotalVoltage, RemainingCapacity, TotalCapacity, Cycles
-- data_pos = data_pos + 2
-- data['cycles_'..addr] = get_int2(binary_data:sub(data_pos, data_pos + 1))

return data
end

function make_message(addr, cid2, command)
local SOI = "\x7E" -- ~
local VER = "\x20"
local EOI = "\x0D" -- carriage return
local len = 0

if command then
len = 2
end
local message = "\x20" .. string.char(addr) .. "\x4A" .. string.char(cid2) .. get_length(len)

local message = VER .. string.char(addr) .. "\x4A" .. string.char(cid2) .. get_length(len)
if command then
message = message .. string.char(command)
end

local payload = to_ascii(message)
payload = "\x7E" .. payload .. get_checksum(payload) .. "\x0D"
payload = SOI .. payload .. get_checksum(payload) .. EOI
return payload
end

function fromhex(str)
function from_hex_to_char(str)
return (str:gsub('..', function(cc)
return string.char(tonumber(cc, 16))
end))
Expand Down Expand Up @@ -143,27 +184,10 @@ function get_length(value)
return string.pack(">i2", val)
end

--[[function hex_dump(str)
local len = string.len(str)
local dump = ""
local hex = ""
local asc = ""
for i = 1, len do
if 1 == i % 8 then
dump = dump .. hex .. asc .. "\n"
hex = string.format("%04x: ", i - 1)
asc = ""
end
local ord = string.byte(str, i)
hex = hex .. string.format("%02x ", ord)
if ord >= 32 and ord <= 126 then
asc = asc .. string.char(ord)
else
asc = asc .. "."
end
function merge_tables(t1, t2)
for key, value in pairs(t2) do
t1[key] = value
end
return dump .. hex
.. string.rep(" ", 8 - len % 8) .. asc
end]]--
end

main()
87 changes: 67 additions & 20 deletions battery_management_systems/pylontech_us2000_rs485/manifest.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
blueprint_spec: device/1.0

display_name: Pylontech Battery US2000 (RS485)
display_name: Pylontech 8 batteries (RS485)
description: Lithium battery
icon: enapter-battery-storage
vendor: pylontech
Expand All @@ -16,78 +16,125 @@ verification_level: verified

communication_module:
product: ENP-RS485
lua_file: firmware.lua
lua:
file: firmware.lua
dependencies:
- enapter-ucm

properties:
serial_number:
vendor:
type: string
display_name: Serial Number
display_name: Vendor
baudrate:
type: integer
display_name: Baudrate
cells:
type: integer
display_name: Cells
groups:
type: integer
display_name: Groups

telemetry:
b0_total_current:
total_current_0:
type: float
unit: amp
display_name: Total Current - Battery 0
b0_total_voltage:
total_voltage_0:
type: float
unit: volt
display_name: Total Voltage - Battery 0
b0_total_power:
total_power_0:
type: float
unit: watt
display_name: Total Power - Battery 0
b0_battery_level:
battery_level_0:
type: float
unit: percent
display_name: Battery Level - Battery 0
b1_total_current:
total_current_1:
type: float
unit: amp
display_name: Total Current - Battery 1
b1_total_voltage:
total_voltage_1:
type: float
unit: volt
display_name: Total Voltage - Battery 1
b1_total_power:
total_power_1:
type: float
unit: watt
display_name: Total Power - Battery 1
b1_battery_level:
battery_level_1:
type: float
unit: percent
display_name: Battery Level - Battery 1
b2_total_current:
total_capacity_2:
type: float
unit: amph
display_name: Total Capacity - Battery 2
remaining_capacity_2:
type: float
unit: amph
display_name: Remaining Capacity - Battery 2
total_current_2:
type: float
unit: amp
display_name: Total Current - Battery 2
b2_total_voltage:
total_voltage_2:
type: float
unit: volt
display_name: Total Voltage - Battery 2
b2_total_power:
total_power_2:
type: float
unit: watt
display_name: Total Power - Battery 2
b2_battery_level:
battery_level_2:
type: float
unit: percent
display_name: Battery Level - Battery 2
b3_total_current:
total_current_3:
type: float
unit: amp
display_name: Total Current - Battery 3
b3_total_voltage:
total_voltage_3:
type: float
unit: volt
display_name: Total Voltage - Battery 3
b3_total_power:
total_power_3:
type: float
unit: watt
display_name: Total Power - Battery 3
b3_battery_level:
battery_level_3:
type: float
unit: percent
display_name: Battery Level - Battery 3

command_groups:
config:
display_name: Configuration

commands:
write_configuration:
populate_values_command: read_configuration
display_name: Configure
group: config
ui:
icon: wrench-outline
arguments:
baudrate:
display_name: Baudrate
description: Transmission rate in kbit/s
type: integer
enum:
- 9600
- 115200
- 500000
required: true
read_configuration:
display_name: Read Configuration
group: config
ui:
icon: wrench-outline

.cloud:
category: batteries