Skip to content

Commit f9ed521

Browse files
authored
Add Seplos v2 BMS support (#115)
* renamed CRCs renamed old crc to modbus added crc for xmodem * added Seplos v2 implementation * avoid detecting Seplos v2 as JBD detailed BT matcher for JBD * fixed tests * corrected modbus CRC function * switched to single machine data * completed tests
1 parent e604544 commit f9ed521

File tree

9 files changed

+498
-45
lines changed

9 files changed

+498
-45
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ This integration allows to monitor Bluetooth Low Energy (BLE) battery management
2929
- Supervolt v3 batteries (show up as `SX1*`…)
3030
- JK BMS, Jikong, (HW version >=11 required)
3131
- Offgridtec LiFePo4 Smart Pro: type A & B (show up as `SmartBat-A`… or `SmartBat-B`…)
32-
- LiTime, Power Queen, and Redodo batteries
32+
- LiTime, Redodo batteries
33+
- Seplos v2 (show up as `BP0`?)
3334
- Seplos v3 (show up as `SP0`… or `SP1`…)
3435

3536
New device types can be easily added via the plugin architecture of this integration. See the [contribution guidelines](CONTRIBUTING.md) for details.
@@ -163,7 +164,7 @@ Once pairing is done, the integration should automatically detect the BMS.
163164
- Add further battery types on [request](https://github.com/patman15/BMS_BLE-HA/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml)
164165

165166
## Thanks to
166-
> [@gkathan](https://github.com/patman15/BMS_BLE-HA/issues/2), [@downset](https://github.com/patman15/BMS_BLE-HA/issues/19), [@gerritb](https://github.com/patman15/BMS_BLE-HA/issues/22), [@Goaheadz](https://github.com/patman15/BMS_BLE-HA/issues/24), [@alros100, @majonessyltetoy](https://github.com/patman15/BMS_BLE-HA/issues/52), [@snipah, @Gruni22](https://github.com/patman15/BMS_BLE-HA/issues/59), [@azisto](https://github.com/patman15/BMS_BLE-HA/issues/78), [@BikeAtor, @Karatzie](https://github.com/patman15/BMS_BLE-HA/issues/57), [@SkeLLLa,@romanshypovskyi](https://github.com/patman15/BMS_BLE-HA/issues/90), [@hacsler](https://github.com/patman15/BMS_BLE-HA/issues/103)
167+
> [@gkathan](https://github.com/patman15/BMS_BLE-HA/issues/2), [@downset](https://github.com/patman15/BMS_BLE-HA/issues/19), [@gerritb](https://github.com/patman15/BMS_BLE-HA/issues/22), [@Goaheadz](https://github.com/patman15/BMS_BLE-HA/issues/24), [@alros100, @majonessyltetoy](https://github.com/patman15/BMS_BLE-HA/issues/52), [@snipah, @Gruni22](https://github.com/patman15/BMS_BLE-HA/issues/59), [@azisto](https://github.com/patman15/BMS_BLE-HA/issues/78), [@BikeAtor, @Karatzie](https://github.com/patman15/BMS_BLE-HA/issues/57), [@SkeLLLa,@romanshypovskyi](https://github.com/patman15/BMS_BLE-HA/issues/90), [@riogrande75, @ebagnoli, @andreas-bulling](https://github.com/patman15/BMS_BLE-HA/issues/101), [@hacsler](https://github.com/patman15/BMS_BLE-HA/issues/103)
167168

168169
for helping with making the integration better.
169170

custom_components/bms_ble/const.py

+22-21
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,40 @@
1010
ATTR_VOLTAGE,
1111
)
1212

13-
BMS_TYPES: Final = [
13+
BMS_TYPES: Final[list[str]] = [
1414
"cbtpwr_bms",
1515
"daly_bms",
16-
"dpwrcore_bms",
1716
"ective_bms",
1817
"ej_bms",
1918
"jbd_bms",
2019
"jikong_bms",
2120
"ogt_bms",
2221
"redodo_bms",
2322
"seplos_bms",
23+
"seplos_v2_bms",
24+
"dpwrcore_bms", # only name filter
2425
] # available BMS types
25-
DOMAIN: Final = "bms_ble"
26+
DOMAIN: Final[str] = "bms_ble"
2627
LOGGER: Final = logging.getLogger(__package__)
27-
UPDATE_INTERVAL: Final = 30 # [s]
28+
UPDATE_INTERVAL: Final[int] = 30 # [s]
2829

2930
# attributes (do not change)
30-
ATTR_CELL_VOLTAGES: Final = "cell_voltages" # [V]
31-
ATTR_CURRENT: Final = "current" # [A]
32-
ATTR_CYCLE_CAP: Final = "cycle_capacity" # [Wh]
33-
ATTR_CYCLE_CHRG: Final = "cycle_charge" # [Ah]
34-
ATTR_CYCLES: Final = "cycles" # [#]
35-
ATTR_DELTA_VOLTAGE: Final = "delta_voltage" # [V]
36-
ATTR_LQ: Final = "link_quality" # [%]
37-
ATTR_POWER: Final = "power" # [W]
38-
ATTR_RSSI: Final = "rssi" # [dBm]
39-
ATTR_RUNTIME: Final = "runtime" # [s]
40-
ATTR_TEMP_SENSORS: Final = "temperature_sensors" # [°C]
31+
ATTR_CELL_VOLTAGES: Final[str] = "cell_voltages" # [V]
32+
ATTR_CURRENT: Final[str] = "current" # [A]
33+
ATTR_CYCLE_CAP: Final[str] = "cycle_capacity" # [Wh]
34+
ATTR_CYCLE_CHRG: Final[str] = "cycle_charge" # [Ah]
35+
ATTR_CYCLES: Final[str] = "cycles" # [#]
36+
ATTR_DELTA_VOLTAGE: Final[str] = "delta_voltage" # [V]
37+
ATTR_LQ: Final[str] = "link_quality" # [%]
38+
ATTR_POWER: Final[str] = "power" # [W]
39+
ATTR_RSSI: Final[str] = "rssi" # [dBm]
40+
ATTR_RUNTIME: Final[str] = "runtime" # [s]
41+
ATTR_TEMP_SENSORS: Final[str] = "temperature_sensors" # [°C]
4142

4243
# temporary dictionary keys (do not change)
43-
KEY_CELL_COUNT: Final = "cell_count" # [#]
44-
KEY_CELL_VOLTAGE: Final = "cell#" # [V]
45-
KEY_DESIGN_CAP: Final = "design_capacity" # [Ah]
46-
KEY_PACK_COUNT: Final = "pack_count" # [#]
47-
KEY_TEMP_SENS: Final = "temp_sensors" # [#]
48-
KEY_TEMP_VALUE: Final = "temp#" # [°C]
44+
KEY_CELL_COUNT: Final[str] = "cell_count" # [#]
45+
KEY_CELL_VOLTAGE: Final[str] = "cell#" # [V]
46+
KEY_DESIGN_CAP: Final[str] = "design_capacity" # [Ah]
47+
KEY_PACK_COUNT: Final[str] = "pack_count" # [#]
48+
KEY_TEMP_SENS: Final[str] = "temp_sensors" # [#]
49+
KEY_TEMP_VALUE: Final[str] = "temp#" # [°C]

custom_components/bms_ble/manifest.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@
6767
"local_name": "$PFLAC*",
6868
"service_uuid": "0000ffe0-0000-1000-8000-00805f9b34fb",
6969
"manufacturer_id": 65535
70-
}
70+
},
71+
{
72+
"local_name": "BP0?",
73+
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
74+
}
7175
],
7276
"codeowners": ["@patman15"],
7377
"config_flow": true,
@@ -78,5 +82,5 @@
7882
"issue_tracker": "https://github.com/patman15/BMS_BLE-HA/issues",
7983
"loggers": ["bleak_retry_connector"],
8084
"requirements": [],
81-
"version": "1.10.0"
85+
"version": "1.11.0"
8286
}

custom_components/bms_ble/plugins/basebms.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,24 @@ async def async_update(self) -> BMSsample:
229229
return data
230230

231231

232-
def crc_xmodem(data: bytearray) -> int:
233-
"""Calculate CRC-16-CCITT XMODEM (ModBus)."""
232+
def crc_modbus(data: bytearray) -> int:
233+
"""Calculate CRC-16-CCITT MODBUS."""
234234
crc: int = 0xFFFF
235235
for i in data:
236236
crc ^= i & 0xFF
237237
for _ in range(8):
238238
crc = (crc >> 1) ^ 0xA001 if crc % 2 else (crc >> 1)
239-
return ((0xFF00 & crc) >> 8) | ((crc & 0xFF) << 8)
239+
return crc & 0xFFFF
240+
241+
242+
def crc_xmodem(data: bytearray) -> int:
243+
"""Calculate CRC-16-CCITT XMODEM."""
244+
crc: int = 0x0000
245+
for byte in data:
246+
crc ^= byte << 8
247+
for _ in range(8):
248+
crc = (crc << 1) ^ 0x1021 if (crc & 0x8000) else (crc << 1)
249+
return crc & 0xFFFF
240250

241251

242252
def crc_sum(frame: bytes) -> int:

custom_components/bms_ble/plugins/daly_bms.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
KEY_TEMP_VALUE,
2727
)
2828

29-
from .basebms import BaseBMS, BMSsample, crc_xmodem
29+
from .basebms import BaseBMS, BMSsample, crc_modbus
3030

3131
BAT_TIMEOUT: Final = 10
3232
LOGGER: Final = logging.getLogger(__name__)
@@ -108,12 +108,12 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
108108
len(data) < BMS.HEAD_LEN
109109
or data[0:2] != BMS.HEAD_READ
110110
or int(data[2]) + 1 != len(data) - len(BMS.HEAD_READ) - BMS.CRC_LEN
111-
or int.from_bytes(data[-2:], byteorder="big") != crc_xmodem(data[:-2])
111+
or int.from_bytes(data[-2:], byteorder="little") != crc_modbus(data[:-2])
112112
):
113113
LOGGER.debug(
114114
"Response data is invalid, CRC: 0x%X != 0x%X",
115-
int.from_bytes(data[-2:], byteorder="big"),
116-
crc_xmodem(data[:-2]),
115+
int.from_bytes(data[-2:], byteorder="little"),
116+
crc_modbus(data[:-2]),
117117
)
118118
self._data = None
119119
else:

custom_components/bms_ble/plugins/seplos_bms.py

+12-12
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
KEY_TEMP_VALUE,
2626
)
2727

28-
from .basebms import BaseBMS, BMSsample, crc_xmodem
28+
from .basebms import BaseBMS, BMSsample, crc_modbus
2929

30-
BAT_TIMEOUT: Final = 5
30+
BAT_TIMEOUT: Final = 10
3131
LOGGER = logging.getLogger(__name__)
3232

3333

@@ -70,7 +70,7 @@ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
7070
"""Intialize private BMS members."""
7171
super().__init__(LOGGER, self._notification_handler, ble_device, reconnect)
7272
self._data: bytearray = bytearray()
73-
self._exp_len: int = 0 # expected packet length
73+
self._pkglen: int = 0 # expected packet length
7474
self._data_final: dict[int, bytearray] = {}
7575
self._pack_count = 0
7676
self._char_write_handle: int | None = None
@@ -129,15 +129,15 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
129129
and data[2] >= BMS.HEAD_LEN + BMS.CRC_LEN
130130
):
131131
self._data = bytearray()
132-
self._exp_len = data[2] + BMS.HEAD_LEN + BMS.CRC_LEN
132+
self._pkglen = data[2] + BMS.HEAD_LEN + BMS.CRC_LEN
133133
elif ( # error message
134134
len(data) == BMS.HEAD_LEN + BMS.CRC_LEN
135135
and data[0] <= self._pack_count
136136
and data[1] & 0x80
137137
):
138138
LOGGER.debug("%s: RX BLE error: %X", self._ble_device.name, int(data[2]))
139139
self._data = bytearray()
140-
self._exp_len = BMS.HEAD_LEN + BMS.CRC_LEN
140+
self._pkglen = BMS.HEAD_LEN + BMS.CRC_LEN
141141

142142
self._data += data
143143
LOGGER.debug(
@@ -148,15 +148,15 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
148148
)
149149

150150
# verify that data long enough
151-
if len(self._data) < self._exp_len:
151+
if len(self._data) < self._pkglen:
152152
return
153153

154-
crc = crc_xmodem(self._data[: self._exp_len - 2])
155-
if int.from_bytes(self._data[self._exp_len - 2 : self._exp_len]) != crc:
154+
crc = crc_modbus(self._data[: self._pkglen - 2])
155+
if int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little") != crc:
156156
LOGGER.debug(
157157
"%s: RX data CRC is invalid: 0x%X != 0x%X",
158158
self._ble_device.name,
159-
int.from_bytes(self._data[self._exp_len - 2 : self._exp_len]),
159+
int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little"),
160160
crc,
161161
)
162162
self._data_final[int(self._data[0])] = bytearray() # reset invalid data
@@ -174,12 +174,12 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
174174
return
175175
else:
176176
self._data_final[int(self._data[0]) << 8 | int(self._data[2])] = self._data
177-
if len(self._data) != self._exp_len:
177+
if len(self._data) != self._pkglen:
178178
LOGGER.debug(
179179
"%s: Wrong data length (%i!=%s): %s",
180180
self._ble_device.name,
181181
len(self._data),
182-
self._exp_len,
182+
self._pkglen,
183183
self._data,
184184
)
185185

@@ -203,7 +203,7 @@ def _cmd(device: int, cmd: int, start: int, count: int) -> bytearray:
203203
frame = bytearray([device, cmd])
204204
frame += bytearray(int.to_bytes(start, 2, byteorder="big"))
205205
frame += bytearray(int.to_bytes(count, 2, byteorder="big"))
206-
frame += bytearray(int.to_bytes(crc_xmodem(frame), 2, byteorder="big"))
206+
frame += bytearray(int.to_bytes(crc_modbus(frame), 2, byteorder="little"))
207207
return frame
208208

209209
async def _async_update(self) -> BMSsample:

0 commit comments

Comments
 (0)