aiobmsble 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiobmsble/bms/__init__.py +1 -0
- aiobmsble/bms/abc_bms.py +164 -0
- aiobmsble/bms/ant_bms.py +196 -0
- aiobmsble/bms/braunpwr_bms.py +167 -0
- aiobmsble/bms/cbtpwr_bms.py +168 -0
- aiobmsble/bms/cbtpwr_vb_bms.py +184 -0
- aiobmsble/bms/daly_bms.py +164 -0
- aiobmsble/bms/dpwrcore_bms.py +207 -0
- aiobmsble/bms/dummy_bms.py +89 -0
- aiobmsble/bms/ecoworthy_bms.py +151 -0
- aiobmsble/bms/ective_bms.py +177 -0
- aiobmsble/bms/ej_bms.py +233 -0
- aiobmsble/bms/felicity_bms.py +139 -0
- aiobmsble/bms/jbd_bms.py +203 -0
- aiobmsble/bms/jikong_bms.py +301 -0
- aiobmsble/bms/neey_bms.py +214 -0
- aiobmsble/bms/ogt_bms.py +214 -0
- aiobmsble/bms/pro_bms.py +144 -0
- aiobmsble/bms/redodo_bms.py +127 -0
- aiobmsble/bms/renogy_bms.py +149 -0
- aiobmsble/bms/renogy_pro_bms.py +105 -0
- aiobmsble/bms/roypow_bms.py +186 -0
- aiobmsble/bms/seplos_bms.py +245 -0
- aiobmsble/bms/seplos_v2_bms.py +205 -0
- aiobmsble/bms/tdt_bms.py +199 -0
- aiobmsble/bms/tianpwr_bms.py +138 -0
- aiobmsble/utils.py +1 -1
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.1.dist-info}/METADATA +3 -2
- aiobmsble-0.2.1.dist-info/RECORD +36 -0
- aiobmsble-0.2.0.dist-info/RECORD +0 -10
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.1.dist-info}/WHEEL +0 -0
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.1.dist-info}/entry_points.txt +0 -0
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,184 @@
|
|
1
|
+
"""Module to support CBT Power VB series BMS."""
|
2
|
+
|
3
|
+
from string import hexdigits
|
4
|
+
from typing import Final
|
5
|
+
|
6
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
7
|
+
from bleak.backends.device import BLEDevice
|
8
|
+
from bleak.uuids import normalize_uuid_str
|
9
|
+
|
10
|
+
from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
|
11
|
+
from aiobmsble.basebms import BaseBMS, lrc_modbus
|
12
|
+
|
13
|
+
|
14
|
+
class BMS(BaseBMS):
|
15
|
+
"""CBT Power VB series battery class implementation."""
|
16
|
+
|
17
|
+
_HEAD: Final[bytes] = b"\x7e"
|
18
|
+
_TAIL: Final[bytes] = b"\x0d"
|
19
|
+
_CMD_VER: Final[int] = 0x11 # TX protocol version
|
20
|
+
_RSP_VER: Final[int] = 0x22 # RX protocol version
|
21
|
+
_LEN_POS: Final[int] = 9
|
22
|
+
_MIN_LEN: Final[int] = _LEN_POS + 3 + len(_HEAD) + len(_TAIL) + 4
|
23
|
+
_MAX_LEN: Final[int] = 255
|
24
|
+
_CELL_POS: Final[int] = 6
|
25
|
+
|
26
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
27
|
+
BMSdp("voltage", 2, 2, False, lambda x: x / 10),
|
28
|
+
BMSdp("current", 0, 2, True, lambda x: x / 10),
|
29
|
+
BMSdp("battery_level", 4, 2, False, lambda x: min(x, 100)),
|
30
|
+
BMSdp("cycles", 7, 2, False),
|
31
|
+
BMSdp("problem_code", 15, 6, False, lambda x: x & 0xFFF000FF000F),
|
32
|
+
)
|
33
|
+
|
34
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
35
|
+
"""Initialize BMS."""
|
36
|
+
super().__init__(ble_device, reconnect)
|
37
|
+
self._exp_len: int = 0
|
38
|
+
|
39
|
+
@staticmethod
|
40
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
41
|
+
"""Provide BluetoothMatcher definition."""
|
42
|
+
return [
|
43
|
+
{ # Creabest
|
44
|
+
"service_uuid": normalize_uuid_str("fff0"),
|
45
|
+
"manufacturer_id": 16963,
|
46
|
+
"connectable": True,
|
47
|
+
},
|
48
|
+
]
|
49
|
+
|
50
|
+
@staticmethod
|
51
|
+
def device_info() -> dict[str, str]:
|
52
|
+
"""Return device information for the battery management system."""
|
53
|
+
return {"manufacturer": "Creabest", "model": "VB series"}
|
54
|
+
|
55
|
+
@staticmethod
|
56
|
+
def uuid_services() -> list[str]:
|
57
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
58
|
+
return [
|
59
|
+
normalize_uuid_str("ffe0"),
|
60
|
+
normalize_uuid_str("ffe5"),
|
61
|
+
]
|
62
|
+
|
63
|
+
@staticmethod
|
64
|
+
def uuid_rx() -> str:
|
65
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
66
|
+
return "ffe4"
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def uuid_tx() -> str:
|
70
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
71
|
+
return "ffe9"
|
72
|
+
|
73
|
+
@staticmethod
|
74
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
75
|
+
return frozenset(
|
76
|
+
{
|
77
|
+
"battery_charging",
|
78
|
+
"delta_voltage",
|
79
|
+
"temperature",
|
80
|
+
"power",
|
81
|
+
"runtime",
|
82
|
+
"cycle_capacity",
|
83
|
+
"cycle_charge",
|
84
|
+
}
|
85
|
+
) # calculate further values from BMS provided set ones
|
86
|
+
|
87
|
+
def _notification_handler(
|
88
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
89
|
+
) -> None:
|
90
|
+
"""Handle the RX characteristics notify event (new data arrives)."""
|
91
|
+
|
92
|
+
if len(data) > BMS._LEN_POS + 4 and data.startswith(BMS._HEAD):
|
93
|
+
self._data = bytearray()
|
94
|
+
try:
|
95
|
+
length: Final[int] = int(data[BMS._LEN_POS : BMS._LEN_POS + 4], 16)
|
96
|
+
self._exp_len = length & 0xFFF
|
97
|
+
if BMS.lencs(length) != length >> 12:
|
98
|
+
self._exp_len = 0
|
99
|
+
self._log.debug("incorrect length checksum.")
|
100
|
+
except ValueError:
|
101
|
+
self._exp_len = 0
|
102
|
+
|
103
|
+
self._data += data
|
104
|
+
self._log.debug(
|
105
|
+
"RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
|
106
|
+
)
|
107
|
+
|
108
|
+
if len(self._data) < self._exp_len + BMS._MIN_LEN:
|
109
|
+
return
|
110
|
+
|
111
|
+
if not self._data.endswith(BMS._TAIL):
|
112
|
+
self._log.debug("incorrect EOF: %s", data)
|
113
|
+
self._data.clear()
|
114
|
+
return
|
115
|
+
|
116
|
+
if not all(chr(c) in hexdigits for c in self._data[1:-1]):
|
117
|
+
self._log.debug("incorrect frame encoding.")
|
118
|
+
self._data.clear()
|
119
|
+
return
|
120
|
+
|
121
|
+
if (ver := bytes.fromhex(self._data[1:3].decode())) != BMS._RSP_VER.to_bytes():
|
122
|
+
self._log.debug("unknown response frame version: 0x%X", int.from_bytes(ver))
|
123
|
+
self._data.clear()
|
124
|
+
return
|
125
|
+
|
126
|
+
if (crc := lrc_modbus(self._data[1:-5])) != int(self._data[-5:-1], 16):
|
127
|
+
self._log.debug(
|
128
|
+
"invalid checksum 0x%X != 0x%X", crc, int(self._data[-5:-1], 16)
|
129
|
+
)
|
130
|
+
self._data.clear()
|
131
|
+
return
|
132
|
+
|
133
|
+
self._data = bytearray(
|
134
|
+
bytes.fromhex(self._data.strip(BMS._HEAD + BMS._TAIL).decode())
|
135
|
+
)
|
136
|
+
self._data_event.set()
|
137
|
+
|
138
|
+
@staticmethod
|
139
|
+
def lencs(length: int) -> int:
|
140
|
+
"""Calculate the length checksum."""
|
141
|
+
return (sum((length >> (i * 4)) & 0xF for i in range(3)) ^ 0xF) + 1 & 0xF
|
142
|
+
|
143
|
+
@staticmethod
|
144
|
+
def _cmd(cmd: int, dev_id: int = 1, data: bytes = b"") -> bytes:
|
145
|
+
"""Assemble a Seplos VB series command."""
|
146
|
+
assert len(data) <= 0xFFF
|
147
|
+
cdat: Final[bytes] = data + int.to_bytes(dev_id)
|
148
|
+
frame = bytearray([BMS._CMD_VER, dev_id, 0x46, cmd])
|
149
|
+
frame.extend(
|
150
|
+
int.to_bytes(len(cdat) * 2 + (BMS.lencs(len(cdat) * 2) << 12), 2, "big")
|
151
|
+
)
|
152
|
+
frame.extend(cdat)
|
153
|
+
frame.extend(
|
154
|
+
int.to_bytes(lrc_modbus(bytearray(frame.hex().upper().encode())), 2, "big")
|
155
|
+
)
|
156
|
+
return BMS._HEAD + frame.hex().upper().encode() + BMS._TAIL
|
157
|
+
|
158
|
+
async def _async_update(self) -> BMSsample:
|
159
|
+
"""Update battery status information."""
|
160
|
+
|
161
|
+
await self._await_reply(BMS._cmd(0x42))
|
162
|
+
result: BMSsample = {"cell_count": self._data[BMS._CELL_POS]}
|
163
|
+
temp_pos: Final[int] = BMS._CELL_POS + result.get("cell_count", 0) * 2 + 1
|
164
|
+
result["temp_sensors"] = self._data[temp_pos]
|
165
|
+
result["cell_voltages"] = BMS._cell_voltages(
|
166
|
+
self._data, cells=result.get("cell_count", 0), start=BMS._CELL_POS + 1
|
167
|
+
)
|
168
|
+
result["temp_values"] = BMS._temp_values(
|
169
|
+
self._data,
|
170
|
+
values=result.get("temp_sensors", 0),
|
171
|
+
start=temp_pos + 1,
|
172
|
+
divider=10,
|
173
|
+
)
|
174
|
+
|
175
|
+
result |= BMS._decode_data(
|
176
|
+
BMS._FIELDS, self._data, offset=temp_pos + 2 * result["temp_sensors"] + 1
|
177
|
+
)
|
178
|
+
|
179
|
+
await self._await_reply(BMS._cmd(0x81, 1, b"\x01\x00"), max_size=20)
|
180
|
+
result["design_capacity"] = (
|
181
|
+
int.from_bytes(self._data[6:8], byteorder="big", signed=False) // 10
|
182
|
+
)
|
183
|
+
|
184
|
+
return result
|
@@ -0,0 +1,164 @@
|
|
1
|
+
"""Module to support Daly Smart BMS."""
|
2
|
+
|
3
|
+
from typing import Final
|
4
|
+
|
5
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
6
|
+
from bleak.backends.device import BLEDevice
|
7
|
+
from bleak.uuids import normalize_uuid_str
|
8
|
+
|
9
|
+
from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
|
10
|
+
from aiobmsble.basebms import BaseBMS, crc_modbus
|
11
|
+
|
12
|
+
|
13
|
+
class BMS(BaseBMS):
|
14
|
+
"""Daly Smart BMS class implementation."""
|
15
|
+
|
16
|
+
HEAD_READ: Final[bytes] = b"\xd2\x03"
|
17
|
+
CMD_INFO: Final[bytes] = b"\x00\x00\x00\x3e\xd7\xb9"
|
18
|
+
MOS_INFO: Final[bytes] = b"\x00\x3e\x00\x09\xf7\xa3"
|
19
|
+
HEAD_LEN: Final[int] = 3
|
20
|
+
CRC_LEN: Final[int] = 2
|
21
|
+
MAX_CELLS: Final[int] = 32
|
22
|
+
MAX_TEMP: Final[int] = 8
|
23
|
+
INFO_LEN: Final[int] = 84 + HEAD_LEN + CRC_LEN + MAX_CELLS + MAX_TEMP
|
24
|
+
MOS_TEMP_POS: Final[int] = HEAD_LEN + 8
|
25
|
+
MOS_NOT_AVAILABLE: Final[tuple[str]] = ("DL-FB4C2E0",)
|
26
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
27
|
+
BMSdp("voltage", 80, 2, False, lambda x: x / 10),
|
28
|
+
BMSdp("current", 82, 2, False, lambda x: (x - 30000) / 10),
|
29
|
+
BMSdp("battery_level", 84, 2, False, lambda x: x / 10),
|
30
|
+
BMSdp("cycle_charge", 96, 2, False, lambda x: x / 10),
|
31
|
+
BMSdp("cell_count", 98, 2, False, lambda x: min(x, BMS.MAX_CELLS)),
|
32
|
+
BMSdp("temp_sensors", 100, 2, False, lambda x: min(x, BMS.MAX_TEMP)),
|
33
|
+
BMSdp("cycles", 102, 2, False, lambda x: x),
|
34
|
+
BMSdp("delta_voltage", 112, 2, False, lambda x: x / 1000),
|
35
|
+
BMSdp("problem_code", 116, 8, False, lambda x: x % 2**64),
|
36
|
+
)
|
37
|
+
|
38
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
39
|
+
"""Intialize private BMS members."""
|
40
|
+
super().__init__(ble_device, reconnect)
|
41
|
+
|
42
|
+
@staticmethod
|
43
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
44
|
+
"""Provide BluetoothMatcher definition."""
|
45
|
+
return [
|
46
|
+
MatcherPattern(
|
47
|
+
local_name="DL-*",
|
48
|
+
service_uuid=BMS.uuid_services()[0],
|
49
|
+
connectable=True,
|
50
|
+
)
|
51
|
+
] + [
|
52
|
+
MatcherPattern(
|
53
|
+
manufacturer_id=m_id,
|
54
|
+
connectable=True,
|
55
|
+
)
|
56
|
+
for m_id in (0x102, 0x104, 0x0302, 0x0303)
|
57
|
+
]
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def device_info() -> dict[str, str]:
|
61
|
+
"""Return device information for the battery management system."""
|
62
|
+
return {"manufacturer": "Daly", "model": "Smart BMS"}
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
def uuid_services() -> list[str]:
|
66
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
67
|
+
return [normalize_uuid_str("fff0")]
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def uuid_rx() -> str:
|
71
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
72
|
+
return "fff1"
|
73
|
+
|
74
|
+
@staticmethod
|
75
|
+
def uuid_tx() -> str:
|
76
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
77
|
+
return "fff2"
|
78
|
+
|
79
|
+
@staticmethod
|
80
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
81
|
+
return frozenset(
|
82
|
+
{
|
83
|
+
"cycle_capacity",
|
84
|
+
"power",
|
85
|
+
"battery_charging",
|
86
|
+
"runtime",
|
87
|
+
"temperature",
|
88
|
+
}
|
89
|
+
)
|
90
|
+
|
91
|
+
def _notification_handler(
|
92
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
93
|
+
) -> None:
|
94
|
+
self._log.debug("RX BLE data: %s", data)
|
95
|
+
|
96
|
+
if (
|
97
|
+
len(data) < BMS.HEAD_LEN
|
98
|
+
or data[0:2] != BMS.HEAD_READ
|
99
|
+
or data[2] + 1 != len(data) - len(BMS.HEAD_READ) - BMS.CRC_LEN
|
100
|
+
):
|
101
|
+
self._log.debug("response data is invalid")
|
102
|
+
return
|
103
|
+
|
104
|
+
if (crc := crc_modbus(data[:-2])) != int.from_bytes(
|
105
|
+
data[-2:], byteorder="little"
|
106
|
+
):
|
107
|
+
self._log.debug(
|
108
|
+
"invalid checksum 0x%X != 0x%X",
|
109
|
+
int.from_bytes(data[-2:], byteorder="little"),
|
110
|
+
crc,
|
111
|
+
)
|
112
|
+
self._data.clear()
|
113
|
+
return
|
114
|
+
|
115
|
+
self._data = data
|
116
|
+
self._data_event.set()
|
117
|
+
|
118
|
+
async def _async_update(self) -> BMSsample:
|
119
|
+
"""Update battery status information."""
|
120
|
+
result: BMSsample = {}
|
121
|
+
if ( # do not query devices that do not support MOS temperature, e.g. Bulltron
|
122
|
+
not self.name or not self.name.startswith(BMS.MOS_NOT_AVAILABLE)
|
123
|
+
):
|
124
|
+
try:
|
125
|
+
# request MOS temperature (possible outcome: response, empty response, no response)
|
126
|
+
await self._await_reply(BMS.HEAD_READ + BMS.MOS_INFO)
|
127
|
+
|
128
|
+
if sum(self._data[BMS.MOS_TEMP_POS :][:2]):
|
129
|
+
self._log.debug("MOS info: %s", self._data)
|
130
|
+
result["temp_values"] = [
|
131
|
+
int.from_bytes(
|
132
|
+
self._data[BMS.MOS_TEMP_POS :][:2],
|
133
|
+
byteorder="big",
|
134
|
+
signed=True,
|
135
|
+
)
|
136
|
+
- 40
|
137
|
+
]
|
138
|
+
except TimeoutError:
|
139
|
+
self._log.debug("no MOS temperature available.")
|
140
|
+
|
141
|
+
await self._await_reply(BMS.HEAD_READ + BMS.CMD_INFO)
|
142
|
+
|
143
|
+
if len(self._data) != BMS.INFO_LEN:
|
144
|
+
self._log.debug("incorrect frame length: %i", len(self._data))
|
145
|
+
return {}
|
146
|
+
|
147
|
+
result |= BMS._decode_data(BMS._FIELDS, self._data, offset=BMS.HEAD_LEN)
|
148
|
+
|
149
|
+
# add temperature sensors
|
150
|
+
result.setdefault("temp_values", []).extend(
|
151
|
+
BMS._temp_values(
|
152
|
+
self._data,
|
153
|
+
values=result.get("temp_sensors", 0),
|
154
|
+
start=64 + BMS.HEAD_LEN,
|
155
|
+
offset=40,
|
156
|
+
)
|
157
|
+
)
|
158
|
+
|
159
|
+
# get cell voltages
|
160
|
+
result["cell_voltages"] = BMS._cell_voltages(
|
161
|
+
self._data, cells=result.get("cell_count", 0), start=BMS.HEAD_LEN
|
162
|
+
)
|
163
|
+
|
164
|
+
return result
|
@@ -0,0 +1,207 @@
|
|
1
|
+
"""Module to support D-powercore Smart BMS."""
|
2
|
+
|
3
|
+
from enum import IntEnum
|
4
|
+
from string import hexdigits
|
5
|
+
from typing import Final
|
6
|
+
|
7
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
8
|
+
from bleak.backends.device import BLEDevice
|
9
|
+
from bleak.uuids import normalize_uuid_str
|
10
|
+
|
11
|
+
from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
|
12
|
+
from aiobmsble.basebms import BaseBMS
|
13
|
+
|
14
|
+
|
15
|
+
class Cmd(IntEnum):
|
16
|
+
"""BMS operation codes."""
|
17
|
+
|
18
|
+
UNLOCKACC = 0x32
|
19
|
+
UNLOCKREJ = 0x33
|
20
|
+
LEGINFO1 = 0x60
|
21
|
+
LEGINFO2 = 0x61
|
22
|
+
CELLVOLT = 0x62
|
23
|
+
UNLOCK = 0x64
|
24
|
+
UNLOCKED = 0x65
|
25
|
+
GETINFO = 0xA0
|
26
|
+
|
27
|
+
|
28
|
+
class BMS(BaseBMS):
|
29
|
+
"""D-powercore Smart BMS class implementation."""
|
30
|
+
|
31
|
+
_PAGE_LEN: Final[int] = 20
|
32
|
+
_MAX_CELLS: Final[int] = 32
|
33
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
34
|
+
BMSdp("voltage", 6, 2, False, lambda x: x / 10, Cmd.LEGINFO1),
|
35
|
+
BMSdp("current", 8, 2, True, lambda x: x, Cmd.LEGINFO1),
|
36
|
+
BMSdp("battery_level", 14, 1, False, lambda x: x, Cmd.LEGINFO1),
|
37
|
+
BMSdp("cycle_charge", 12, 2, False, lambda x: x / 1000, Cmd.LEGINFO1),
|
38
|
+
BMSdp(
|
39
|
+
"temperature",
|
40
|
+
12,
|
41
|
+
2,
|
42
|
+
False,
|
43
|
+
lambda x: round(x * 0.1 - 273.15, 1),
|
44
|
+
Cmd.LEGINFO2,
|
45
|
+
),
|
46
|
+
BMSdp(
|
47
|
+
"cell_count", 6, 1, False, lambda x: min(x, BMS._MAX_CELLS), Cmd.CELLVOLT
|
48
|
+
),
|
49
|
+
BMSdp("cycles", 8, 2, False, lambda x: x, Cmd.LEGINFO2),
|
50
|
+
BMSdp("problem_code", 15, 1, False, lambda x: x & 0xFF, Cmd.LEGINFO1),
|
51
|
+
)
|
52
|
+
_CMDS: Final[set[Cmd]] = {Cmd(field.idx) for field in _FIELDS}
|
53
|
+
|
54
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
55
|
+
"""Intialize private BMS members."""
|
56
|
+
super().__init__(ble_device, reconnect)
|
57
|
+
assert self._ble_device.name is not None # required for unlock
|
58
|
+
self._data_final: dict[int, bytearray] = {}
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
62
|
+
"""Provide BluetoothMatcher definition."""
|
63
|
+
return [
|
64
|
+
{
|
65
|
+
"local_name": pattern,
|
66
|
+
"service_uuid": BMS.uuid_services()[0],
|
67
|
+
"connectable": True,
|
68
|
+
}
|
69
|
+
for pattern in ("DXB-*", "TBA-*")
|
70
|
+
]
|
71
|
+
|
72
|
+
@staticmethod
|
73
|
+
def device_info() -> dict[str, str]:
|
74
|
+
"""Return device information for the battery management system."""
|
75
|
+
return {"manufacturer": "D-powercore", "model": "Smart BMS"}
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def uuid_services() -> list[str]:
|
79
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
80
|
+
return [normalize_uuid_str("fff0")]
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def uuid_rx() -> str:
|
84
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
85
|
+
return "fff4"
|
86
|
+
|
87
|
+
@staticmethod
|
88
|
+
def uuid_tx() -> str:
|
89
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
90
|
+
return "fff3"
|
91
|
+
|
92
|
+
@staticmethod
|
93
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
94
|
+
return frozenset(
|
95
|
+
{
|
96
|
+
"battery_charging",
|
97
|
+
"cycle_capacity",
|
98
|
+
"delta_voltage",
|
99
|
+
"power",
|
100
|
+
"runtime",
|
101
|
+
}
|
102
|
+
)
|
103
|
+
|
104
|
+
async def _notification_handler(
|
105
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
106
|
+
) -> None:
|
107
|
+
self._log.debug("RX BLE data: %s", data)
|
108
|
+
|
109
|
+
if len(data) != BMS._PAGE_LEN:
|
110
|
+
self._log.debug("invalid page length (%i)", len(data))
|
111
|
+
return
|
112
|
+
|
113
|
+
# ignore ACK responses
|
114
|
+
if data[0] & 0x80:
|
115
|
+
self._log.debug("ignore acknowledge message")
|
116
|
+
return
|
117
|
+
|
118
|
+
# acknowledge received frame
|
119
|
+
await self._await_reply(
|
120
|
+
bytes([data[0] | 0x80]) + data[1:], wait_for_notify=False
|
121
|
+
)
|
122
|
+
|
123
|
+
size: Final[int] = data[0]
|
124
|
+
page: Final[int] = data[1] >> 4
|
125
|
+
maxpg: Final[int] = data[1] & 0xF
|
126
|
+
|
127
|
+
if page == 1:
|
128
|
+
self._data.clear()
|
129
|
+
|
130
|
+
self._data += data[2 : size + 2]
|
131
|
+
|
132
|
+
self._log.debug("(%s): %s", "start" if page == 1 else "cnt.", data)
|
133
|
+
|
134
|
+
if page == maxpg:
|
135
|
+
if (crc := BMS._crc(self._data[3:-4])) != int.from_bytes(
|
136
|
+
self._data[-4:-2], byteorder="big"
|
137
|
+
):
|
138
|
+
self._log.debug(
|
139
|
+
"incorrect checksum: 0x%X != 0x%X",
|
140
|
+
int.from_bytes(self._data[-4:-2], byteorder="big"),
|
141
|
+
crc,
|
142
|
+
)
|
143
|
+
self._data.clear()
|
144
|
+
self._data_final = {} # reset invalid data
|
145
|
+
return
|
146
|
+
|
147
|
+
self._data_final[self._data[3]] = self._data.copy()
|
148
|
+
self._data_event.set()
|
149
|
+
|
150
|
+
@staticmethod
|
151
|
+
def _crc(data: bytearray) -> int:
|
152
|
+
return sum(data) + 8
|
153
|
+
|
154
|
+
@staticmethod
|
155
|
+
def _cmd(cmd: Cmd, data: bytes) -> bytes:
|
156
|
+
frame: bytearray = bytearray([cmd.value, 0x00, 0x00]) + data
|
157
|
+
checksum: Final[int] = BMS._crc(frame)
|
158
|
+
frame = (
|
159
|
+
bytearray([0x3A, 0x03, 0x05])
|
160
|
+
+ frame
|
161
|
+
+ bytes([(checksum >> 8) & 0xFF, checksum & 0xFF, 0x0D, 0x0A])
|
162
|
+
)
|
163
|
+
frame = bytearray([len(frame) + 2, 0x11]) + frame
|
164
|
+
frame += bytes(BMS._PAGE_LEN - len(frame))
|
165
|
+
|
166
|
+
return bytes(frame)
|
167
|
+
|
168
|
+
async def _init_connection(
|
169
|
+
self, char_notify: BleakGATTCharacteristic | int | str | None = None
|
170
|
+
) -> None:
|
171
|
+
"""Connect to the BMS and setup notification if not connected."""
|
172
|
+
await super()._init_connection()
|
173
|
+
|
174
|
+
# unlock BMS if not TBA version
|
175
|
+
if self.name.startswith("TBA-"):
|
176
|
+
return
|
177
|
+
|
178
|
+
if not all(c in hexdigits for c in self.name[-4:]):
|
179
|
+
self._log.debug("unable to unlock BMS")
|
180
|
+
return
|
181
|
+
|
182
|
+
pwd = int(self.name[-4:], 16)
|
183
|
+
await self._await_reply(
|
184
|
+
BMS._cmd(
|
185
|
+
Cmd.UNLOCK,
|
186
|
+
bytes([(pwd >> 8) & 0xFF, pwd & 0xFF]),
|
187
|
+
),
|
188
|
+
wait_for_notify=False,
|
189
|
+
)
|
190
|
+
|
191
|
+
async def _async_update(self) -> BMSsample:
|
192
|
+
"""Update battery status information."""
|
193
|
+
for request in BMS._CMDS:
|
194
|
+
await self._await_reply(self._cmd(request, b""))
|
195
|
+
|
196
|
+
if not BMS._CMDS.issubset(set(self._data_final.keys())):
|
197
|
+
raise ValueError("incomplete response set")
|
198
|
+
|
199
|
+
result: BMSsample = BMS._decode_data(BMS._FIELDS, self._data_final)
|
200
|
+
result["cell_voltages"] = BMS._cell_voltages(
|
201
|
+
self._data_final[Cmd.CELLVOLT],
|
202
|
+
cells=result.get("cell_count", 0),
|
203
|
+
start=7,
|
204
|
+
)
|
205
|
+
|
206
|
+
self._data_final.clear()
|
207
|
+
return result
|
@@ -0,0 +1,89 @@
|
|
1
|
+
"""Module to support Dummy BMS."""
|
2
|
+
|
3
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
4
|
+
from bleak.backends.device import BLEDevice
|
5
|
+
from bleak.uuids import normalize_uuid_str
|
6
|
+
|
7
|
+
from aiobmsble import BMSsample, BMSvalue, MatcherPattern
|
8
|
+
from aiobmsble.basebms import BaseBMS
|
9
|
+
|
10
|
+
|
11
|
+
class BMS(BaseBMS):
|
12
|
+
"""Dummy BMS implementation."""
|
13
|
+
|
14
|
+
# _HEAD: Final[bytes] = b"\x55" # beginning of frame
|
15
|
+
# _TAIL: Final[bytes] = b"\xAA" # end of frame
|
16
|
+
# _FRAME_LEN: Final[int] = 10 # length of frame, including SOF and checksum
|
17
|
+
|
18
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
19
|
+
"""Initialize BMS."""
|
20
|
+
super().__init__(ble_device, reconnect)
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
24
|
+
"""Provide BluetoothMatcher definition."""
|
25
|
+
return [{"local_name": "dummy", "connectable": True}] # TODO
|
26
|
+
|
27
|
+
@staticmethod
|
28
|
+
def device_info() -> dict[str, str]:
|
29
|
+
"""Return device information for the battery management system."""
|
30
|
+
return {"manufacturer": "Dummy Manufacturer", "model": "dummy model"} # TODO
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def uuid_services() -> list[str]:
|
34
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
35
|
+
return [normalize_uuid_str("0000")] # TODO: change service UUID here!
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def uuid_rx() -> str:
|
39
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
40
|
+
return "0000" # TODO: change RX characteristic UUID here!
|
41
|
+
|
42
|
+
@staticmethod
|
43
|
+
def uuid_tx() -> str:
|
44
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
45
|
+
return "0000" # TODO: change TX characteristic UUID here!
|
46
|
+
|
47
|
+
@staticmethod
|
48
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
49
|
+
return frozenset(
|
50
|
+
{"power", "battery_charging"}
|
51
|
+
) # calculate further values from BMS provided set ones
|
52
|
+
|
53
|
+
def _notification_handler(
|
54
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
55
|
+
) -> None:
|
56
|
+
"""Handle the RX characteristics notify event (new data arrives)."""
|
57
|
+
# self._log.debug("RX BLE data: %s", data)
|
58
|
+
|
59
|
+
# *******************************************************
|
60
|
+
# # TODO: Do things like checking correctness of frame here
|
61
|
+
# # and store it into a instance variable, e.g. self._data
|
62
|
+
# # Below are some examples of how to do it
|
63
|
+
# # Have a look at the BMS base class for function to use,
|
64
|
+
# # take a look at other implementations for more details
|
65
|
+
# *******************************************************
|
66
|
+
|
67
|
+
# if not data.startswith(BMS._HEAD):
|
68
|
+
# self._log.debug("incorrect SOF")
|
69
|
+
# return
|
70
|
+
|
71
|
+
# if (crc := crc_sum(self._data[:-1])) != self._data[-1]:
|
72
|
+
# self._log.debug("invalid checksum 0x%X != 0x%X", self._data[-1], crc)
|
73
|
+
# return
|
74
|
+
|
75
|
+
# self._data = data.copy()
|
76
|
+
# self._data_event.set()
|
77
|
+
|
78
|
+
async def _async_update(self) -> BMSsample:
|
79
|
+
"""Update battery status information."""
|
80
|
+
self._log.debug("replace with command to UUID %s", BMS.uuid_tx())
|
81
|
+
# await self._await_reply(b"<some_command>")
|
82
|
+
|
83
|
+
# # TODO: parse data from self._data here
|
84
|
+
|
85
|
+
return {
|
86
|
+
"voltage": 12,
|
87
|
+
"current": 1.5,
|
88
|
+
"temperature": 27.182,
|
89
|
+
} # TODO: fixed values, replace parsed data
|