Skip to content

Commit

Permalink
Merge branch '101-support-for-seplos-v2-bms' into 102-add-tdt-bsm-sup…
Browse files Browse the repository at this point in the history
…port
  • Loading branch information
patman15 committed Dec 15, 2024
2 parents d9c9329 + c4a2b2a commit 9bf7a7c
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 196 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ This integration allows to monitor Bluetooth Low Energy (BLE) battery management
- Supervolt v1 batteries
- Elektronicx batteries (show up as `LT-`…)
- Ective batteries
- JBD BMS, Jiabaida
- accurat batteries
- Supervolt v3 batteries
- JBD BMS, Jiabaida (show up as `SP..S`…)
- accurat batteries (show up as `GJ-`…)
- 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
- Seplos v2 (show up as `BP0`…)
- Seplos v2 (show up as `BP00`…)
- Seplos v3 (show up as `SP0`… or `SP1`…)
- TDT BMS (show up as `XDZN`…)

Expand Down Expand Up @@ -163,7 +163,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), [@riogrande75, @ebagnoli, @andreas-bulling](https://github.com/patman15/BMS_BLE-HA/issues/101), [@hacsler](https://github.com/patman15/BMS_BLE-HA/issues/103), [@goblinmaks](https://github.com/patman15/BMS_BLE-HA/issues/102)
> [@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
40 changes: 20 additions & 20 deletions custom_components/bms_ble/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ATTR_VOLTAGE,
)

BMS_TYPES: Final = [
BMS_TYPES: Final[list[str]] = [
"cbtpwr_bms",
"daly_bms",
"ective_bms",
Expand All @@ -24,27 +24,27 @@
"dpwrcore_bms", # only name filter
"jbd_bms", # only UUID 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]
23 changes: 18 additions & 5 deletions custom_components/bms_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
"name": "BLE Battery Management",
"bluetooth": [
{
"local_name": "SmartBat-A*",
"service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb"
},
{
"local_name": "SmartBat-B*",
"local_name": "SmartBat-[AB]*",
"service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb"
},
{
Expand All @@ -19,8 +15,25 @@
"manufacturer_id": 2917
},
{
"local_name": "SP0?S*",
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
},
{
"local_name": "SP1?S*",
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
},
{
"local_name": "SP2?S*",
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
},
{
"local_name": "GJ-*",
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
},
{
"local_name": "SX1*",
"service_uuid": "0000ff00-0000-1000-8000-00805f9b34fb"
},
{
"local_name": "SP0*",
"service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/bms_ble/plugins/basebms.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def crc_modbus(data: bytearray) -> int:
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."""
Expand Down
4 changes: 2 additions & 2 deletions custom_components/bms_ble/plugins/daly_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ 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_modbus(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"),
int.from_bytes(data[-2:], byteorder="little"),
crc_modbus(data[:-2]),
)
self._data = None
Expand Down
4 changes: 3 additions & 1 deletion custom_components/bms_ble/plugins/jbd_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ def matcher_dict_list() -> list[dict[str, Any]]:
"""Provide BluetoothMatcher definition."""
return [
{
"local_name": pattern,
"service_uuid": BMS.uuid_services()[0],
"connectable": True,
},
}
for pattern in ["SP0?S*", "SP1?S*", "SP2?S*", "GJ-*", "SX1*"]
]

@staticmethod
Expand Down
13 changes: 3 additions & 10 deletions custom_components/bms_ble/plugins/ogt_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,10 @@ def matcher_dict_list() -> list[dict[str, Any]]:
"""Return a list of Bluetooth matchers."""
return [
{
"local_name": "SmartBat-A*",
"local_name": "SmartBat-[AB]*",
"service_uuid": BMS.uuid_services()[0],
"connectable": True,
},
{
"local_name": "SmartBat-B*",
"service_uuid": BMS.uuid_services()[0],
"connectable": True,
},
}
]

@staticmethod
Expand Down Expand Up @@ -140,9 +135,7 @@ async def _async_update(self) -> BMSsample:
try:
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
except TimeoutError:
LOGGER.debug(
"Reading %s timed out", self._REGISTERS[key][BMS.IDX_NAME]
)
LOGGER.debug("Reading %s timed out", self._REGISTERS[key][BMS.IDX_NAME])
if key > 48 and f"{KEY_CELL_VOLTAGE}{64-key}" not in self._values:
break

Expand Down
22 changes: 11 additions & 11 deletions custom_components/bms_ble/plugins/seplos_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

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_modbus(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_modbus(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
35 changes: 13 additions & 22 deletions custom_components/bms_ble/plugins/seplos_v2_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from .basebms import BaseBMS, BMSsample, crc_xmodem

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


class BMS(BaseBMS):
Expand All @@ -38,22 +38,21 @@ class BMS(BaseBMS):
_TAIL: Final[int] = 0x0D
_CMD_VER: Final[int] = 0x10
_RSP_VER: Final[int] = 0x14
_INFO_LEN: Final[int] = 10 # minimal frame length
_CELLS_POS: Final[int] = 9
_MAX_SUBS: Final[int] = 15
_MIN_LEN: Final[int] = 10
_MAX_SUBS: Final[int] = 0xF
_FIELDS: Final[
list[tuple[str, int, int, int, bool, Callable[[int], int | float]]]
] = [
(ATTR_VOLTAGE, 0x62, 25, 2, False, lambda x: float(x / 100)),
(ATTR_CURRENT, 0x62, 23, 2, True, lambda x: float(x / 100)),
(ATTR_CYCLE_CHRG, 0x62, 27, 2, False, lambda x: float(x / 100)),
(ATTR_CURRENT, 0x62, 23, 2, True, lambda x: float(x / 10)),
(ATTR_CYCLE_CHRG, 0x62, 27, 2, False, lambda x: float(x / 10)),
(ATTR_CYCLES, 0x62, 36, 2, False, lambda x: x),
(ATTR_BATTERY_LEVEL, 0x62, 32, 2, False, lambda x: float(x / 10)),
(ATTR_TEMPERATURE, 0x62, 21, 2, True, lambda x: (x - 2731.5) / 10),
(KEY_CELL_COUNT, 0x62, 9, 1, False, lambda x: x),
(KEY_PACK_COUNT, 0x51, 42, 1, False, lambda x: min(x, BMS._MAX_SUBS)),
(KEY_TEMP_SENS, 0x62, 14, 1, False, lambda x: x),
] # Protocol Seplos V2 (parallel data 0x62)
] # Protocol Seplos V2 (parallel data 0x62, device manufacturer info 0x51)
_CMDS: Final[list[int]] = list({field[1] for field in _FIELDS})

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down Expand Up @@ -106,14 +105,12 @@ def _calc_values() -> set[str]:

def _notification_handler(self, _sender, data: bytearray) -> None:
"""Handle the RX characteristics notify event (new data arrives)."""
LOGGER.debug("%s: Received BLE data: %s", self.name, data)

if (
data[0] == BMS._HEAD
and len(data) > BMS._INFO_LEN
and len(data) > BMS._MIN_LEN
and len(self._data) >= self._exp_len
):
self._exp_len = BMS._INFO_LEN + int.from_bytes(data[5:7])
self._exp_len = BMS._MIN_LEN + int.from_bytes(data[5:7])
self._data = bytearray()

self._data += data
Expand Down Expand Up @@ -165,13 +162,12 @@ def _notification_handler(self, _sender, data: bytearray) -> None:

@staticmethod
def _cmd(cmd: int, data: bytearray = bytearray()) -> bytearray:
"""Assemble a Seplos BMS command."""
"""Assemble a Seplos V2 BMS command."""
assert cmd in (0x47, 0x51, 0x61, 0x62, 0x04) # allow only read commands
frame = bytearray([BMS._HEAD, BMS._CMD_VER, 0x0, 0x46, cmd]) # fixed version
frame += len(data).to_bytes(2, "big", signed=False) + data
frame += bytearray(int.to_bytes(crc_xmodem(frame[1:]), 2, byteorder="big"))
frame += bytearray([BMS._TAIL])
LOGGER.debug("TX cmd: %s", frame.hex(" "))
return frame

@staticmethod
Expand All @@ -190,15 +186,13 @@ def _cell_voltages(data: bytearray, offset: int = 0) -> dict[str, float]:
return {
f"{KEY_CELL_VOLTAGE}{idx+offset}": float(
int.from_bytes(
data[
BMS._CELLS_POS + 1 + idx * 2 : BMS._CELLS_POS + 1 + idx * 2 + 2
],
data[10 + idx * 2 : 10 + idx * 2 + 2],
byteorder="big",
signed=False,
)
)
/ 1000
for idx in range(data[BMS._CELLS_POS])
for idx in range(data[9])
}

async def _async_update(self) -> BMSsample:
Expand All @@ -207,11 +201,8 @@ async def _async_update(self) -> BMSsample:
for cmd in BMS._CMDS:
await self._client.write_gatt_char(BMS.uuid_tx(), data=BMS._cmd(cmd))
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
# check if a valid frame was received otherwise terminate immediately
if cmd not in self._data_final:
return {}

result = BMS._decode_data(self._data_final)
result: BMSsample = BMS._decode_data(self._data_final)

total_cells: int = 0
for pack in range(int(result.get(KEY_PACK_COUNT, 0) + 1)):
Expand All @@ -229,7 +220,7 @@ async def _async_update(self) -> BMSsample:
round(max(pack_cells.values()) - min(pack_cells.values()), 3),
)
}
total_cells += self._data_final[0x61][BMS._CELLS_POS]
total_cells += self._data_final[0x61][9]

self._data_final.clear()

Expand Down
Loading

0 comments on commit 9bf7a7c

Please sign in to comment.