Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Seplos v2 BMS support #115

Merged
merged 24 commits into from
Dec 18, 2024
Merged
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ This integration allows to monitor Bluetooth Low Energy (BLE) battery management
- Supervolt v3 batteries (show up as `SX1*`…)
- JK BMS, Jikong, (HW version >=11 required)
- Offgridtec LiFePo4 Smart Pro: type A & B (show up as `SmartBat-A`… or `SmartBat-B`…)
- LiTime, Power Queen, and Redodo batteries
- LiTime, Redodo batteries
- Seplos v2 (show up as `BP0`?)
- Seplos v3 (show up as `SP0`… or `SP1`…)

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

## Thanks to
> [@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)
> [@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)

for helping with making the integration better.

Expand Down
43 changes: 22 additions & 21 deletions custom_components/bms_ble/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,40 @@
ATTR_VOLTAGE,
)

BMS_TYPES: Final = [
BMS_TYPES: Final[list[str]] = [
"cbtpwr_bms",
"daly_bms",
"dpwrcore_bms",
"ective_bms",
"ej_bms",
"jbd_bms",
"jikong_bms",
"ogt_bms",
"redodo_bms",
"seplos_bms",
"seplos_v2_bms",
"dpwrcore_bms", # only name filter
] # available BMS types
DOMAIN: Final = "bms_ble"
DOMAIN: Final[str] = "bms_ble"
LOGGER: Final = logging.getLogger(__package__)
UPDATE_INTERVAL: Final = 30 # [s]
UPDATE_INTERVAL: Final[int] = 30 # [s]

# attributes (do not change)
ATTR_CELL_VOLTAGES: Final = "cell_voltages" # [V]
ATTR_CURRENT: Final = "current" # [A]
ATTR_CYCLE_CAP: Final = "cycle_capacity" # [Wh]
ATTR_CYCLE_CHRG: Final = "cycle_charge" # [Ah]
ATTR_CYCLES: Final = "cycles" # [#]
ATTR_DELTA_VOLTAGE: Final = "delta_voltage" # [V]
ATTR_LQ: Final = "link_quality" # [%]
ATTR_POWER: Final = "power" # [W]
ATTR_RSSI: Final = "rssi" # [dBm]
ATTR_RUNTIME: Final = "runtime" # [s]
ATTR_TEMP_SENSORS: Final = "temperature_sensors" # [°C]
ATTR_CELL_VOLTAGES: Final[str] = "cell_voltages" # [V]
ATTR_CURRENT: Final[str] = "current" # [A]
ATTR_CYCLE_CAP: Final[str] = "cycle_capacity" # [Wh]
ATTR_CYCLE_CHRG: Final[str] = "cycle_charge" # [Ah]
ATTR_CYCLES: Final[str] = "cycles" # [#]
ATTR_DELTA_VOLTAGE: Final[str] = "delta_voltage" # [V]
ATTR_LQ: Final[str] = "link_quality" # [%]
ATTR_POWER: Final[str] = "power" # [W]
ATTR_RSSI: Final[str] = "rssi" # [dBm]
ATTR_RUNTIME: Final[str] = "runtime" # [s]
ATTR_TEMP_SENSORS: Final[str] = "temperature_sensors" # [°C]

# temporary dictionary keys (do not change)
KEY_CELL_COUNT: Final = "cell_count" # [#]
KEY_CELL_VOLTAGE: Final = "cell#" # [V]
KEY_DESIGN_CAP: Final = "design_capacity" # [Ah]
KEY_PACK_COUNT: Final = "pack_count" # [#]
KEY_TEMP_SENS: Final = "temp_sensors" # [#]
KEY_TEMP_VALUE: Final = "temp#" # [°C]
KEY_CELL_COUNT: Final[str] = "cell_count" # [#]
KEY_CELL_VOLTAGE: Final[str] = "cell#" # [V]
KEY_DESIGN_CAP: Final[str] = "design_capacity" # [Ah]
KEY_PACK_COUNT: Final[str] = "pack_count" # [#]
KEY_TEMP_SENS: Final[str] = "temp_sensors" # [#]
KEY_TEMP_VALUE: Final[str] = "temp#" # [°C]
8 changes: 6 additions & 2 deletions custom_components/bms_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@
"local_name": "$PFLAC*",
"service_uuid": "0000ffe0-0000-1000-8000-00805f9b34fb",
"manufacturer_id": 65535
}
},
{
"local_name": "BP0?",
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
}
],
"codeowners": ["@patman15"],
"config_flow": true,
Expand All @@ -78,5 +82,5 @@
"issue_tracker": "https://github.com/patman15/BMS_BLE-HA/issues",
"loggers": ["bleak_retry_connector"],
"requirements": [],
"version": "1.10.0"
"version": "1.11.0"
}
16 changes: 13 additions & 3 deletions custom_components/bms_ble/plugins/basebms.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,24 @@ async def async_update(self) -> BMSsample:
return data


def crc_xmodem(data: bytearray) -> int:
"""Calculate CRC-16-CCITT XMODEM (ModBus)."""
def crc_modbus(data: bytearray) -> int:
"""Calculate CRC-16-CCITT MODBUS."""
crc: int = 0xFFFF
for i in data:
crc ^= i & 0xFF
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc % 2 else (crc >> 1)
return ((0xFF00 & crc) >> 8) | ((crc & 0xFF) << 8)
return crc & 0xFFFF


def crc_xmodem(data: bytearray) -> int:
"""Calculate CRC-16-CCITT XMODEM."""
crc: int = 0x0000
for byte in data:
crc ^= byte << 8
for _ in range(8):
crc = (crc << 1) ^ 0x1021 if (crc & 0x8000) else (crc << 1)
return crc & 0xFFFF


def crc_sum(frame: bytes) -> int:
Expand Down
8 changes: 4 additions & 4 deletions custom_components/bms_ble/plugins/daly_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
KEY_TEMP_VALUE,
)

from .basebms import BaseBMS, BMSsample, crc_xmodem
from .basebms import BaseBMS, BMSsample, crc_modbus

BAT_TIMEOUT: Final = 10
LOGGER: Final = logging.getLogger(__name__)
Expand Down Expand Up @@ -108,12 +108,12 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
len(data) < BMS.HEAD_LEN
or data[0:2] != BMS.HEAD_READ
or int(data[2]) + 1 != len(data) - len(BMS.HEAD_READ) - BMS.CRC_LEN
or int.from_bytes(data[-2:], byteorder="big") != crc_xmodem(data[:-2])
or int.from_bytes(data[-2:], byteorder="little") != crc_modbus(data[:-2])
):
LOGGER.debug(
"Response data is invalid, CRC: 0x%X != 0x%X",
int.from_bytes(data[-2:], byteorder="big"),
crc_xmodem(data[:-2]),
int.from_bytes(data[-2:], byteorder="little"),
crc_modbus(data[:-2]),
)
self._data = None
else:
Expand Down
24 changes: 12 additions & 12 deletions custom_components/bms_ble/plugins/seplos_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
KEY_TEMP_VALUE,
)

from .basebms import BaseBMS, BMSsample, crc_xmodem
from .basebms import BaseBMS, BMSsample, crc_modbus

BAT_TIMEOUT: Final = 5
BAT_TIMEOUT: Final = 10
LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -70,7 +70,7 @@ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
"""Intialize private BMS members."""
super().__init__(LOGGER, self._notification_handler, ble_device, reconnect)
self._data: bytearray = bytearray()
self._exp_len: int = 0 # expected packet length
self._pkglen: int = 0 # expected packet length
self._data_final: dict[int, bytearray] = {}
self._pack_count = 0
self._char_write_handle: int | None = None
Expand Down Expand Up @@ -129,15 +129,15 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
and data[2] >= BMS.HEAD_LEN + BMS.CRC_LEN
):
self._data = bytearray()
self._exp_len = data[2] + BMS.HEAD_LEN + BMS.CRC_LEN
self._pkglen = data[2] + BMS.HEAD_LEN + BMS.CRC_LEN
elif ( # error message
len(data) == BMS.HEAD_LEN + BMS.CRC_LEN
and data[0] <= self._pack_count
and data[1] & 0x80
):
LOGGER.debug("%s: RX BLE error: %X", self._ble_device.name, int(data[2]))
self._data = bytearray()
self._exp_len = BMS.HEAD_LEN + BMS.CRC_LEN
self._pkglen = BMS.HEAD_LEN + BMS.CRC_LEN

self._data += data
LOGGER.debug(
Expand All @@ -148,15 +148,15 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
)

# verify that data long enough
if len(self._data) < self._exp_len:
if len(self._data) < self._pkglen:
return

crc = crc_xmodem(self._data[: self._exp_len - 2])
if int.from_bytes(self._data[self._exp_len - 2 : self._exp_len]) != crc:
crc = crc_modbus(self._data[: self._pkglen - 2])
if int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little") != crc:
LOGGER.debug(
"%s: RX data CRC is invalid: 0x%X != 0x%X",
self._ble_device.name,
int.from_bytes(self._data[self._exp_len - 2 : self._exp_len]),
int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little"),
crc,
)
self._data_final[int(self._data[0])] = bytearray() # reset invalid data
Expand All @@ -174,12 +174,12 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
return
else:
self._data_final[int(self._data[0]) << 8 | int(self._data[2])] = self._data
if len(self._data) != self._exp_len:
if len(self._data) != self._pkglen:
LOGGER.debug(
"%s: Wrong data length (%i!=%s): %s",
self._ble_device.name,
len(self._data),
self._exp_len,
self._pkglen,
self._data,
)

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

async def _async_update(self) -> BMSsample:
Expand Down
Loading
Loading