Skip to content

Commit

Permalink
Finalize Pylontech US200
Browse files Browse the repository at this point in the history
  • Loading branch information
anataty committed Dec 16, 2021
1 parent 1022cfe commit 36bc138
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 80 deletions.
11 changes: 6 additions & 5 deletions battery_management_systems/pylontech_us2000_can/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Pylontech US2000 (CAN bus)

This _Enapter Device Blueprint_ integrates **Pylontech US2000** - lithium battery control and monitoring via [CAN bus](https://developers.enapter.com/docs/reference/ucm/can). The device can also support RS-485 and RS-232 communication interfaces (configured by vendor).
This [Enapter Device Blueprint](https://github.com/Enapter/marketplace#blue_book-enapter-device-blueprints) integrates **Pylontech US2000** - lithium battery control and monitoring via [CAN bus](https://developers.enapter.com/docs/reference/ucm/can). The device can also support RS-485 and RS-232 communication interfaces (configured by vendor).

Use [Enapter ENP-CAN](https://handbook.enapter.com/modules/ENP-CAN/ENP-CAN.html) module for physical connection. See [connection instructions](https://handbook.enapter.com/modules/ENP-CAN/ENP-CAN.html#connection-examples) in the module manual.
## Connect to Enapter

## CAN bus Communication Interface Parameters

- Baud rate: `500` kbps.
- Sign up Enapter Cloud to using [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 [Enapter ENP-CAN](https://handbook.enapter.com/modules/ENP-CAN/ENP-CAN.html) module for physical connection. See [connection instructions](https://handbook.enapter.com/modules/ENP-CAN/ENP-CAN.html#connection-examples) in the module manual.
- [Add ENP-CAN to your site](https://handbook.enapter.com/software/mobile/android_mobile_app.html#adding-sites-and-devices) using the mobile app.
- [Upload](https://developers.enapter.com/docs/tutorial/uploading-blueprint/) this blueprint to ENP-CAN.

## References

Expand Down
156 changes: 119 additions & 37 deletions battery_management_systems/pylontech_us2000_can/firmware.lua
Original file line number Diff line number Diff line change
@@ -1,29 +1,121 @@
--- CAN bus interface baud rate
BAUD_RATE = 500 -- kbps

telemetry = {}

function main()
local err = can.init(500, can_handler)
local err = can.init(BAUD_RATE, can_handler)
if err and err ~= 0 then
enapter.log("CAN failed: "..err.." "..can.err_to_str(err), "error", true)
scheduler.add(5000, function()
enapter.send_telemetry({ status = 'error', alerts = {'can_init_failed'} })
end)
else
scheduler.add(30000, send_properties)
scheduler.add(1000, send_telemetry)
end

scheduler.add(30000, properties)
scheduler.add(1000, metrics)
end

function properties()
function send_properties()
enapter.send_properties({ vendor = "PylonTech", model = "US2000" })
end

function metrics()
if next(telemetry) ~= nil then
-- Holds the number of `send_telemetry` that had an empty `telemetry`
local missed_telemetry_count = 0

function send_telemetry()
if telemetry[1] ~= nil then
enapter.send_telemetry(telemetry)
-- Cleanup telemetry and let it be refilled by `can_handler`
telemetry = {}
missed_telemetry_count = 0
else
missed_telemetry_count = missed_telemetry_count + 1
if missed_telemetry_count > 5 then
enapter.send_telemetry({
status = 'read_error',
alerts = {'can_fail'}
})
end
end
end

function can_handler(msg_id, data)
local alerts = {}
local status = "ok"

if msg_id == 0x351 then
status, alerts = handle_msg_0x351(data, status, alerts)
end

if msg_id == 0x355 then
status, alerts = handle_msg_0x355(data, status, alerts)
end

if msg_id == 0x356 then
status, alerts = handle_msg_0x356(data, status, alerts)
end

if msg_id == 0x359 then
status, alerts = handle_msg_0x359(data, status, alerts)
end

if msg_id == 0x35C then
status, alerts = handle_msg_0x35C(data, status, alerts)
end

telemetry["status"] = status
telemetry["alerts"] = alerts
end

function handle_msg_0x351(data, status, alerts)
local ok, err = pcall(function()
local byte_pair1, byte_pair2, byte_pair3, byte_pair4 = string.unpack("I2i2i2I2", data)
telemetry["battery_charge_voltage"] = byte_pair1 / 10.0
telemetry["charge_current_limit"] = byte_pair2 / 10.0
telemetry["discharge_current_limit"] = byte_pair3 / 10.0
telemetry["discharge_voltage"] = byte_pair4 / 10.0
end)
if not ok then
enapter.log("Reading message 0x351 failed: "..err, "error")
table.insert(alerts, "can_fail")
status = "read_error"
end
return status, alerts
end

function handle_msg_0x355(data, status, alerts)
local ok, err = pcall(function()
local soc, soh = string.unpack("I2I2", data)
telemetry["soh"] = soh
telemetry["soc"] = soc
end)
if not ok then
enapter.log("Reading message 0x355 failed: "..err, "error")
table.insert(alerts, "can_fail")
status = "read_error"
end
return status, alerts
end

function handle_msg_0x356(data, status, alerts)
local ok, err = pcall(function()
local voltage, current, temp = string.unpack("I2i2i2", data)
telemetry["voltage"] = voltage / 100.0
telemetry["total_current"] = current / 10.0
telemetry["average_cell_temperature"] = temp / 10.0
end)
if not ok then
enapter.log("Reading message 0x356 failed: "..err, "error")
table.insert(alerts, "can_fail")
status = "read_error"
end
return status, alerts
end

function handle_msg_0x359(data, status, alerts)
local ok, err = pcall(function()
local byte0, byte1, byte2, byte3 = string.unpack("<I1<I1<I1<I1", data)
local alerts = {}

if byte0 & 2 ~=0 then
table.insert(alerts, "cell_or_module_over_voltage")
Expand All @@ -44,7 +136,7 @@ function can_handler(msg_id, data)
telemetry["has_protection1"] = false
else
telemetry["has_protection1"] = true
telemetry["status"] = "warning"
status = "warning"
end

if byte1 & 1 ~=0 then
Expand All @@ -57,7 +149,7 @@ function can_handler(msg_id, data)
telemetry["has_protection2"] = false
else
telemetry["has_protection2"] = true
telemetry["status"] = "warning"
status = "warning"
end

if byte2 & 2 ~=0 then
Expand All @@ -79,7 +171,7 @@ function can_handler(msg_id, data)
telemetry["has_alarm1"] = false
else
telemetry["has_alarm1"] = true
telemetry["status"] = "warning"
status = "warning"
end

if byte3 & 1 ~=0 then
Expand All @@ -92,35 +184,19 @@ function can_handler(msg_id, data)
telemetry["has_alarm2"] = false
else
telemetry["has_alarm2"] = true
telemetry["status"] = "warning"
end

if #alerts == 0 then
telemetry["status"] = "ok"
status = "warning"
end

telemetry["alerts"] = alerts
end)
if not ok then
enapter.log("Reading message 0x351 failed: "..err, "error")
table.insert(alerts, "can_fail")
status = "read_error"
end
return status, alerts
end

if msg_id == 0x351 then
local byte_pair1, byte_pair2, byte_pair3, byte_pair4 = string.unpack("I2i2i2I2", data)
telemetry["battery_charge_voltage"] = byte_pair1 / 10.0
telemetry["charge_current_limit"] = byte_pair2 / 10.0
telemetry["discharge_current_limit"] = byte_pair3 / 10.0
telemetry["discharge_voltage"] = byte_pair4 / 10.0
end
if msg_id == 0x355 then
local soc, soh = string.unpack("I2I2", data)
telemetry["soh"] = soh
telemetry["soc"] = soc
end
if msg_id == 0x356 then
local voltage, current, temp = string.unpack("I2i2i2", data)
telemetry["voltage"] = voltage / 100.0
telemetry["total_current"] = current / 10.0
telemetry["average_cell_temperature"] = temp / 10.0
end
if msg_id == 0x35C then
function handle_msg_0x35C(data, status, alerts)
local ok, err = pcall(function()
local byte = string.unpack("<I1", data)
if byte & 8 ~= 0 then
telemetry["request_force_charge"] = true
Expand All @@ -147,7 +223,13 @@ function can_handler(msg_id, data)
else
telemetry["charge_enable"] = false
end
end)
if not ok then
enapter.log("Reading message 0x35C failed: "..err, "error")
table.insert(alerts, "can_fail")
status = "read_error"
end
return status, alerts
end

main()
9 changes: 9 additions & 0 deletions battery_management_systems/pylontech_us2000_can/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ telemetry:
enum:
- ok
- warning
- read_error
- error
has_protection1:
type: boolean
display_name: Has Protection 1
Expand Down Expand Up @@ -88,6 +90,13 @@ telemetry:
unit: celsius

alerts:
can_init_failed:
display_name: CAN Driver Initialization Failed
description: CAN driver could not initialize. Try to reboot device or contact Enapter support.
severity: error
can_fail:
display_name: CAN Communication Failed
severity: error
cell_or_module_over_voltage:
display_name: Protection 1 - Cell or Module Over Voltage
severity: warning
Expand Down
13 changes: 6 additions & 7 deletions battery_management_systems/pylontech_us2000_rs485/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
# Pylontech US200 (RS485)

This _Enapter Device Blueprint_ integrates **Pylontech US2000** - lithium battery control and monitoring via [RS-485 communication interface](https://developers.enapter.com/docs/reference/ucm/rs485). The device can also support CAN communication interface (configured by vendor).
This [Enapter Device Blueprint](https://github.com/Enapter/marketplace#blue_book-enapter-device-blueprints) integrates **Pylontech US2000** - lithium battery control and monitoring via [RS-485 communication interface](https://developers.enapter.com/docs/reference/ucm/rs485). The device can also support CAN communication interface (configured by vendor).

Use [Enapter ENP-RS485](https://handbook.enapter.com/modules/ENP-RS485/ENP-RS485.html) module for physical connection. See [connection instructions](https://handbook.enapter.com/modules/ENP-RS485/ENP-RS485.html#connection-examples) in the module manual.
## Connect to Enapter

## RS-485 Communication Interface Parameters
- Sign up Enapter Cloud to using [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 [Enapter ENP-RS485](https://handbook.enapter.com/modules/ENP-RS485/ENP-RS485.html) module for physical connection. See [connection instructions](https://handbook.enapter.com/modules/ENP-RS485/ENP-RS485.html#connection-examples) in the module manual.
- [Add ENP-RS485 your site](https://handbook.enapter.com/software/mobile/android_mobile_app.html#adding-sites-and-devices) using the mobile app.
- [Upload](https://developers.enapter.com/docs/tutorial/uploading-blueprint/) this blueprint to ENP-CAN.

- Baud rate: `9600` bps;
- Data bits: `8`;
- Parity: `N` (no parity);
- Stop bits: `1`.

## References

Expand Down
42 changes: 11 additions & 31 deletions battery_management_systems/pylontech_us2000_rs485/firmware.lua
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
-- Pylontech US2000 Battery
-- Serial over RS485
-- Baud rate = 9600 bps, 8 data bits, no parity, 1 stop bit
-- RS485 serial interface parameters
BAUD_RATE = 9600
DATA_BITS = 8
PARITY = "N"
STOP_BITS = 1

function main()
local result = rs485.init(9600, 8, "N", 1)
local result = rs485.init(BAUD_RATE, 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()
function send_properties()
enapter.send_properties({ vendor = "Pylontech", model = "US2000" })
end

function telemetry()
function send_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)
Expand Down Expand Up @@ -44,6 +46,7 @@ function telemetry()
end
end

-- @addr 2-13 (2 - master battery)
function get_analog_value(addr)
local message = make_message(addr, 0x42, 2)

Expand Down Expand Up @@ -143,27 +146,4 @@ 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
end
return dump .. hex
.. string.rep(" ", 8 - len % 8) .. asc
end]]--

main()

0 comments on commit 36bc138

Please sign in to comment.