Skip to content

Commit

Permalink
Add Boqiang BMS support using protocol version 2.6 (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
syssi committed Mar 12, 2023
1 parent a688bd3 commit a304d61
Show file tree
Hide file tree
Showing 30 changed files with 991 additions and 64 deletions.
37 changes: 33 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,40 @@ jobs:
- name: Write secrets.yaml
shell: bash
run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > secrets.yaml'
- run: |
- name: Write tests/secrets.yaml
shell: bash
run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > tests/secrets.yaml'
- name: Validate esp8266 example configurations
run: |
esphome -s external_components_source components config esp8266-example.yaml
esphome -s external_components_source components config esp8266-example-debug.yaml
esphome -s external_components_source components config esp8266-example-faker.yaml
esphome -s external_components_source components config esp8266-boqiang-example.yaml
esphome -s external_components_source components config esp8266-boqiang-example-debug.yaml
esphome -s external_components_source components config esp8266-boqiang-example-faker.yaml
esphome -s external_components_source components config esp8266-boqiang-bms001-example.yaml
esphome -s external_components_source components config esp8266-boqiang-bms001-example-debug.yaml
esphome -s external_components_source components config esp8266-boqiang-bms001-example-faker.yaml
esphome -s external_components_source components config esp8266-example-multiple-battery-banks.yaml
- run: |
- name: Validate esp32 example configurations
run: |
esphome -s external_components_source components config esp32-example.yaml
esphome -s external_components_source components config esp32-example-debug.yaml
esphome -s external_components_source components config esp32-example-faker.yaml
esphome -s external_components_source components config esp32-boqiang-example.yaml
esphome -s external_components_source components config esp32-boqiang-example-debug.yaml
esphome -s external_components_source components config esp32-boqiang-example-faker.yaml
esphome -s external_components_source components config esp32-boqiang-bms001-example.yaml
esphome -s external_components_source components config esp32-boqiang-bms001-example-debug.yaml
esphome -s external_components_source components config esp32-boqiang-bms001-example-faker.yaml
- name: Validate test configurations
run: |
esphome -s external_components_source ../components config tests/esp8266-boqiang-emulator.yaml
esphome -s external_components_source ../components config tests/esp8266-boqiang-test.yaml
esphome -s external_components_source ../components config tests/esp8266-fake-bms.yaml
esphome -s external_components_source ../components config tests/esp8266-protocol-version.yaml
esphome -s external_components_source ../components config tests/esp8266-requests.yaml
esphome -s external_components_source ../components config tests/esp8266-seplos-emulator.yaml
esphome-compile:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -206,8 +231,12 @@ jobs:
- name: Write secrets.yaml
shell: bash
run: 'echo -e "wifi_ssid: ssid\nwifi_password: password\nmqtt_host: host\nmqtt_username: username\nmqtt_password: password" > secrets.yaml'
- run: |
- name: Compile esp8266 example configurations
run: |
esphome -s external_components_source components compile esp8266-example-faker.yaml
esphome -s external_components_source components compile esp8266-boqiang-example-faker.yaml
esphome -s external_components_source components compile esp8266-example-multiple-battery-banks.yaml
- run: |
- name: Compile esp32 example configurations
run: |
esphome -s external_components_source components compile esp32-example-faker.yaml
esphome -s external_components_source components compile esp32-boqiang-example-faker.yaml
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ ESPHome component to monitor Seplos BMS via UART or RS485
* 1101-ZH26 (reported by [@faizan-elite](https://github.com/syssi/esphome-seplos-bms/issues/2))
* 1101-MZ02 (reported by [@fajera81](https://github.com/syssi/esphome-seplos-bms/discussions/33))
* 1101-10E-SP76-16S (reported by [@tobox](https://github.com/syssi/esphome-seplos-bms/discussions/42))
* Boqiang BMS007-LD43-16S-HW (reported by [@xdilian](https://github.com/syssi/esphome-seplos-bms/discussions/43)) requires custom settings
```
protocol_version: 0x26
override_pack: 1
```
* Boqiang BMS001-HS01-15S (reported by [@xdilian](https://github.com/syssi/esphome-seplos-bms/discussions/43)) requires custom settings
```
protocol_version: 0x26
override_pack: 1
override_cell_count: 10
```

## Untested devices

Expand Down
17 changes: 13 additions & 4 deletions components/seplos_bms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
MULTI_CONF = True

CONF_SEPLOS_BMS_ID = "seplos_bms_id"
CONF_ENABLE_FAKE_TRAFFIC = "enable_fake_traffic"
CONF_OVERRIDE_CELL_COUNT = "override_cell_count"

DEFAULT_PROTOCOL_VERSION = 0x20
DEFAULT_ADDRESS = 0x00

seplos_bms_ns = cg.esphome_ns.namespace("seplos_bms")
SeplosBms = seplos_bms_ns.class_(
Expand All @@ -19,11 +22,17 @@
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SeplosBms),
cv.Optional(CONF_ENABLE_FAKE_TRAFFIC, default=False): cv.boolean,
cv.Optional(CONF_OVERRIDE_CELL_COUNT, default=0): cv.int_range(
min=0, max=16
),
}
)
.extend(cv.polling_component_schema("10s"))
.extend(seplos_modbus.seplos_modbus_device_schema(0x00))
.extend(
seplos_modbus.seplos_modbus_device_schema(
DEFAULT_PROTOCOL_VERSION, DEFAULT_ADDRESS
)
)
)


Expand All @@ -32,4 +41,4 @@ async def to_code(config):
await cg.register_component(var, config)
await seplos_modbus.register_seplos_modbus_device(var, config)

cg.add(var.set_enable_fake_traffic(config[CONF_ENABLE_FAKE_TRAFFIC]))
cg.add(var.set_override_cell_count(config[CONF_OVERRIDE_CELL_COUNT]))
48 changes: 21 additions & 27 deletions components/seplos_bms/seplos_bms.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,26 @@ void SeplosBms::on_seplos_modbus_data(const std::vector<uint8_t> &data) {
// 14 77 142 (0x8E)
// 15 79 146 (0x92)
// 16 81 150 (0x96)
// 24 97 182 (0xB6) guessed
if (data.size() >= 65 && data[8] >= 8 && data[8] <= 24) {
if (data.size() >= 44 && data[8] >= 8 && data[8] <= 16) {
this->on_telemetry_data_(data);
return;
}

ESP_LOGW(TAG, "Unhandled data received: %s", format_hex_pretty(&data.front(), data.size()).c_str());
ESP_LOGW(TAG, "Unhandled data received (data_len: 0x%02X): %s", data[5],
format_hex_pretty(&data.front(), data.size()).c_str());
}

void SeplosBms::on_telemetry_data_(const std::vector<uint8_t> &data) {
auto seplos_get_16bit = [&](size_t i) -> uint16_t {
return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
};

ESP_LOGI(TAG, "Telemetry frame received");
ESP_LOGI(TAG, "Telemetry frame (%d bytes) received", data.size());
ESP_LOGVV(TAG, " %s", format_hex_pretty(&data.front(), data.size()).c_str());

// ->
// 0x2000460010960001100CD70CE90CF40CD60CEF0CE50CE10CDC0CE90CF00CE80CEF0CEA0CDA0CDE0CD8060BA60BA00B970BA60BA50BA2FD5C14A0344E0A426803134650004603E8149F0000000000000000
// 0x26004600307600011000000000000000000000000000000000000000000000000000000000000000000608530853085308530BAC0B9000000000002D0213880001E6B8
//
// *Data*
//
Expand All @@ -43,8 +45,9 @@ void SeplosBms::on_telemetry_data_(const std::vector<uint8_t> &data) {
// 5 0x96 Data length LENID 150 / 2 = 75
// 6 0x00 Data flag
// 7 0x01 Command group
ESP_LOGV(TAG, "Command group: %d", data[7]);
// 8 0x10 Number of cells 16
uint8_t cells = data[8];
uint8_t cells = (this->override_cell_count_) ? this->override_cell_count_ : data[8];

ESP_LOGV(TAG, "Number of cells: %d", cells);
// 9 0x0C 0xD7 Cell voltage 1 3287 * 0.001f = 3.287 V
Expand Down Expand Up @@ -122,12 +125,24 @@ void SeplosBms::on_telemetry_data_(const std::vector<uint8_t> &data) {
// 65 0x46 0x50 Rated capacity 18000 * 0.01f = 180.00 Ah
this->publish_state_(this->rated_capacity_sensor_, (float) seplos_get_16bit(offset + 11) * 0.01f);

if (data.size() < offset + 13 + 2) {
return;
}

// 67 0x00 0x46 Number of cycles 70
this->publish_state_(this->charging_cycles_sensor_, (float) seplos_get_16bit(offset + 13));

if (data.size() < offset + 15 + 2) {
return;
}

// 69 0x03 0xE8 State of health 1000 * 0.1f = 100.0 %
this->publish_state_(this->state_of_health_sensor_, (float) seplos_get_16bit(offset + 15) * 0.1f);

if (data.size() < offset + 17 + 2) {
return;
}

// 71 0x14 0x9F Port voltage 5279 * 0.01f = 52.79 V
this->publish_state_(this->port_voltage_sensor_, (float) seplos_get_16bit(offset + 17) * 0.01f);

Expand All @@ -139,7 +154,6 @@ void SeplosBms::on_telemetry_data_(const std::vector<uint8_t> &data) {

void SeplosBms::dump_config() {
ESP_LOGCONFIG(TAG, "SeplosBms:");
ESP_LOGCONFIG(TAG, " Fake traffic enabled: %s", YESNO(this->enable_fake_traffic_));
LOG_SENSOR("", "Minimum Cell Voltage", this->min_cell_voltage_sensor_);
LOG_SENSOR("", "Maximum Cell Voltage", this->max_cell_voltage_sensor_);
LOG_SENSOR("", "Minimum Voltage Cell", this->min_voltage_cell_sensor_);
Expand Down Expand Up @@ -187,27 +201,7 @@ float SeplosBms::get_setup_priority() const {
return setup_priority::BUS - 1.0f;
}

void SeplosBms::update() {
this->send(0x42, this->address_);

if (this->enable_fake_traffic_) {
// 14S telemetry
this->on_seplos_modbus_data({0x20, 0x00, 0x46, 0x00, 0xA0, 0x8E, 0x00, 0x01, 0x0E, 0x0F, 0xBB, 0x0F, 0xB6,
0x0F, 0xAC, 0x0F, 0xAC, 0x0F, 0xB9, 0x0F, 0xB6, 0x0F, 0xB9, 0x0F, 0xBD, 0x0F,
0xBD, 0x0F, 0xBD, 0x0F, 0xBC, 0x0F, 0xBF, 0x0F, 0xBF, 0x0F, 0xBF, 0x06, 0x0B,
0x2D, 0x0B, 0x2A, 0x0B, 0x30, 0x0B, 0x2A, 0x0B, 0x52, 0x0B, 0x2F, 0x00, 0x37,
0x16, 0x03, 0xCA, 0x04, 0x0A, 0xDA, 0xC0, 0x03, 0x9B, 0xDA, 0xC0, 0x00, 0x02,
0x03, 0xE8, 0x16, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});

// 16S telemetry
this->on_seplos_modbus_data({0x20, 0x00, 0x46, 0x00, 0x10, 0x96, 0x00, 0x01, 0x10, 0x0C, 0xD8, 0x0C, 0xE8, 0x0C,
0xF4, 0x0C, 0xD7, 0x0C, 0xEE, 0x0C, 0xE5, 0x0C, 0xE1, 0x0C, 0xDD, 0x0C, 0xE9, 0x0C,
0xF0, 0x0C, 0xE8, 0x0C, 0xEF, 0x0C, 0xEB, 0x0C, 0xDA, 0x0C, 0xDE, 0x0C, 0xD9, 0x06,
0x0B, 0xA6, 0x0B, 0xA0, 0x0B, 0x97, 0x0B, 0xA6, 0x0B, 0xA5, 0x0B, 0xA2, 0xFD, 0x72,
0x14, 0xA0, 0x34, 0x4A, 0x0A, 0x42, 0x68, 0x03, 0x13, 0x46, 0x50, 0x00, 0x46, 0x03,
0xE8, 0x14, 0x9F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x7C});
}
}
void SeplosBms::update() { this->send(0x42, this->pack_); }

void SeplosBms::publish_state_(binary_sensor::BinarySensor *binary_sensor, const bool &state) {
if (binary_sensor == nullptr)
Expand Down
5 changes: 3 additions & 2 deletions components/seplos_bms/seplos_bms.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ class SeplosBms : public PollingComponent, public seplos_modbus::SeplosModbusDev

void set_errors_text_sensor(text_sensor::TextSensor *errors_text_sensor) { errors_text_sensor_ = errors_text_sensor; }

void set_enable_fake_traffic(bool enable_fake_traffic) { enable_fake_traffic_ = enable_fake_traffic; }
void set_override_cell_count(uint8_t override_cell_count) { this->override_cell_count_ = override_cell_count; }

void on_seplos_modbus_data(const std::vector<uint8_t> &data) override;

void dump_config() override;
Expand Down Expand Up @@ -109,7 +110,7 @@ class SeplosBms : public PollingComponent, public seplos_modbus::SeplosModbusDev
sensor::Sensor *temperature_sensor_{nullptr};
} temperatures_[6];

bool enable_fake_traffic_;
uint8_t override_cell_count_{0};

void publish_state_(binary_sensor::BinarySensor *binary_sensor, const bool &state);
void publish_state_(sensor::Sensor *sensor, float value);
Expand Down
23 changes: 16 additions & 7 deletions components/seplos_modbus/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_FLOW_CONTROL_PIN, CONF_ID
from esphome.cpp_helpers import gpio_pin_expression

Expand All @@ -14,6 +14,8 @@

CONF_SEPLOS_MODBUS_ID = "seplos_modbus_id"
CONF_RX_TIMEOUT = "rx_timeout"
CONF_PROTOCOL_VERSION = "protocol_version"
CONF_OVERRIDE_PACK = "override_pack"

CONFIG_SCHEMA = (
cv.Schema(
Expand Down Expand Up @@ -42,19 +44,26 @@ async def to_code(config):
cg.add(var.set_flow_control_pin(pin))


def seplos_modbus_device_schema(default_address):
def seplos_modbus_device_schema(default_protocol_version, default_address):
schema = {
cv.GenerateID(CONF_SEPLOS_MODBUS_ID): cv.use_id(SeplosModbus),
cv.Optional(CONF_ADDRESS, default=default_address): cv.hex_uint8_t,
cv.Optional(
CONF_PROTOCOL_VERSION, default=default_protocol_version
): cv.hex_uint8_t,
cv.Optional(CONF_OVERRIDE_PACK): cv.hex_uint8_t,
}
if default_address is None:
schema[cv.Required(CONF_ADDRESS)] = cv.hex_uint8_t
else:
schema[cv.Optional(CONF_ADDRESS, default=default_address)] = cv.hex_uint8_t
return cv.Schema(schema)


async def register_seplos_modbus_device(var, config):
parent = await cg.get_variable(config[CONF_SEPLOS_MODBUS_ID])
cg.add(var.set_parent(parent))
cg.add(var.set_address(config[CONF_ADDRESS]))
cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION]))
cg.add(parent.register_device(var))

if CONF_OVERRIDE_PACK in config:
cg.add(var.set_pack(config[CONF_OVERRIDE_PACK]))
else:
cg.add(var.set_pack(config[CONF_ADDRESS]))
20 changes: 10 additions & 10 deletions components/seplos_modbus/seplos_modbus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ bool SeplosModbus::parse_seplos_modbus_byte_(uint8_t byte) {

// Start of frame
if (raw[0] != 0x7E) {
ESP_LOGW(TAG, "Invalid header.");
ESP_LOGW(TAG, "Invalid header");

// return false to reset buffer
return false;
Expand All @@ -98,7 +98,7 @@ bool SeplosModbus::parse_seplos_modbus_byte_(uint8_t byte) {
uint16_t remote_crc = uint16_t(ascii_hex_to_byte(raw[at - 4], raw[at - 3])) << 8 |
(uint16_t(ascii_hex_to_byte(raw[at - 2], raw[at - 1])) << 0);
if (computed_crc != remote_crc) {
ESP_LOGW(TAG, "SeplosBms CRC Check failed! %04X != %04X", computed_crc, remote_crc);
ESP_LOGW(TAG, "CRC Check failed! 0x%04X != 0x%04X", computed_crc, remote_crc);
return false;
}

Expand Down Expand Up @@ -135,19 +135,19 @@ float SeplosModbus::get_setup_priority() const {
return setup_priority::BUS - 1.0f;
}

void SeplosModbus::send(uint8_t address, uint8_t function, uint8_t value) {
void SeplosModbus::send(uint8_t protocol_version, uint8_t address, uint8_t function, uint8_t value) {
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true);

const uint16_t lenid = lchksum(1 * 2);
std::vector<uint8_t> data;
data.push_back(0x20); // VER
data.push_back(address); // ADDR
data.push_back(0x46); // CID1
data.push_back(function); // CID2 (0x42)
data.push_back(lenid >> 8); // LCHKSUM (0xE0)
data.push_back(lenid >> 0); // LENGTH (0x02)
data.push_back(value); // VALUE (0x00)
data.push_back(protocol_version); // VER
data.push_back(address); // ADDR
data.push_back(0x46); // CID1
data.push_back(function); // CID2 (0x42)
data.push_back(lenid >> 8); // LCHKSUM (0xE0)
data.push_back(lenid >> 0); // LENGTH (0x02)
data.push_back(value); // VALUE (0x00)

const uint16_t frame_len = data.size();
std::string payload = "~"; // SOF (0x7E)
Expand Down
10 changes: 8 additions & 2 deletions components/seplos_modbus/seplos_modbus.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class SeplosModbus : public uart::UARTDevice, public Component {

float get_setup_priority() const override;

void send(uint8_t address, uint8_t function, uint8_t value);
void send(uint8_t protocol_version, uint8_t address, uint8_t function, uint8_t value);
void set_rx_timeout(uint16_t rx_timeout) { rx_timeout_ = rx_timeout; }
void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }

Expand All @@ -43,14 +43,20 @@ class SeplosModbusDevice {
public:
void set_parent(SeplosModbus *parent) { parent_ = parent; }
void set_address(uint8_t address) { address_ = address; }
void set_pack(uint8_t pack) { pack_ = pack; }
void set_protocol_version(uint8_t protocol_version) { protocol_version_ = protocol_version; }
virtual void on_seplos_modbus_data(const std::vector<uint8_t> &data) = 0;
void send(uint8_t function, uint8_t value) { this->parent_->send(this->address_, function, value); }
void send(uint8_t function, uint8_t value) {
this->parent_->send(this->protocol_version_, this->address_, function, value);
}

protected:
friend SeplosModbus;

SeplosModbus *parent_;
uint8_t address_;
uint8_t pack_;
uint8_t protocol_version_;
};

} // namespace seplos_modbus
Expand Down
21 changes: 21 additions & 0 deletions esp32-boqiang-bms001-example-debug.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<<: !include esp32-boqiang-bms001-example.yaml

logger:
level: DEBUG

uart:
id: uart0
# Please set the default baudrate of your Seplos BMS model here. It's sometimes 19200 baud instead of 9600.
baud_rate: 9600
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
# The increased RX buffer size is important because
# the full BMS response must fit into the buffer
rx_buffer_size: 384
debug:
dummy_receiver: false
direction: BOTH
# after:
# delimiter: "\r"
# sequence:
# - lambda: UARTDebug::log_string(direction, bytes);
Loading

0 comments on commit a304d61

Please sign in to comment.