aiobmsble 0.2.0__py3-none-any.whl → 0.2.2__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/__init__.py +5 -1
- aiobmsble/__main__.py +5 -1
- aiobmsble/basebms.py +10 -1
- aiobmsble/bms/__init__.py +5 -0
- aiobmsble/bms/abc_bms.py +168 -0
- aiobmsble/bms/ant_bms.py +200 -0
- aiobmsble/bms/braunpwr_bms.py +171 -0
- aiobmsble/bms/cbtpwr_bms.py +172 -0
- aiobmsble/bms/cbtpwr_vb_bms.py +188 -0
- aiobmsble/bms/daly_bms.py +168 -0
- aiobmsble/bms/dpwrcore_bms.py +211 -0
- aiobmsble/bms/dummy_bms.py +93 -0
- aiobmsble/bms/ecoworthy_bms.py +155 -0
- aiobmsble/bms/ective_bms.py +181 -0
- aiobmsble/bms/ej_bms.py +237 -0
- aiobmsble/bms/felicity_bms.py +143 -0
- aiobmsble/bms/jbd_bms.py +207 -0
- aiobmsble/bms/jikong_bms.py +305 -0
- aiobmsble/bms/neey_bms.py +218 -0
- aiobmsble/bms/ogt_bms.py +218 -0
- aiobmsble/bms/pro_bms.py +148 -0
- aiobmsble/bms/redodo_bms.py +131 -0
- aiobmsble/bms/renogy_bms.py +152 -0
- aiobmsble/bms/renogy_pro_bms.py +109 -0
- aiobmsble/bms/roypow_bms.py +190 -0
- aiobmsble/bms/seplos_bms.py +249 -0
- aiobmsble/bms/seplos_v2_bms.py +209 -0
- aiobmsble/bms/tdt_bms.py +203 -0
- aiobmsble/bms/tianpwr_bms.py +142 -0
- aiobmsble/utils.py +16 -6
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/METADATA +3 -2
- aiobmsble-0.2.2.dist-info/RECORD +36 -0
- aiobmsble-0.2.0.dist-info/RECORD +0 -10
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/WHEEL +0 -0
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/entry_points.txt +0 -0
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/top_level.txt +0 -0
aiobmsble/bms/ej_bms.py
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
"""Module to support E&J Technology BMS.
|
2
|
+
|
3
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
4
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
5
|
+
"""
|
6
|
+
|
7
|
+
from enum import IntEnum
|
8
|
+
from string import hexdigits
|
9
|
+
from typing import Final, Literal
|
10
|
+
|
11
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
12
|
+
from bleak.backends.device import BLEDevice
|
13
|
+
|
14
|
+
from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
|
15
|
+
from aiobmsble.basebms import BaseBMS
|
16
|
+
|
17
|
+
|
18
|
+
class Cmd(IntEnum):
|
19
|
+
"""BMS operation codes."""
|
20
|
+
|
21
|
+
RT = 0x2
|
22
|
+
CAP = 0x10
|
23
|
+
|
24
|
+
|
25
|
+
class BMS(BaseBMS):
|
26
|
+
"""E&J Technology BMS implementation."""
|
27
|
+
|
28
|
+
_BT_MODULE_MSG: Final[bytes] = bytes([0x41, 0x54, 0x0D, 0x0A]) # BLE module message
|
29
|
+
_IGNORE_CRC: Final[str] = "libattU"
|
30
|
+
_HEAD: Final[bytes] = b"\x3a"
|
31
|
+
_TAIL: Final[bytes] = b"\x7e"
|
32
|
+
_MAX_CELLS: Final[int] = 16
|
33
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
34
|
+
BMSdp(
|
35
|
+
"current", 89, 8, False, lambda x: ((x >> 16) - (x & 0xFFFF)) / 100, Cmd.RT
|
36
|
+
),
|
37
|
+
BMSdp("battery_level", 123, 2, False, lambda x: x, Cmd.RT),
|
38
|
+
BMSdp("cycle_charge", 15, 4, False, lambda x: x / 10, Cmd.CAP),
|
39
|
+
BMSdp(
|
40
|
+
"temperature", 97, 2, False, lambda x: x - 40, Cmd.RT
|
41
|
+
), # only 1st sensor relevant
|
42
|
+
BMSdp("cycles", 115, 4, False, lambda x: x, Cmd.RT),
|
43
|
+
BMSdp(
|
44
|
+
"problem_code", 105, 4, False, lambda x: x & 0x0FFC, Cmd.RT
|
45
|
+
), # mask status bits
|
46
|
+
)
|
47
|
+
|
48
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
49
|
+
"""Initialize BMS."""
|
50
|
+
super().__init__(ble_device, reconnect)
|
51
|
+
self._data_final: bytearray = bytearray()
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
55
|
+
"""Provide BluetoothMatcher definition."""
|
56
|
+
return (
|
57
|
+
[ # Lithtech Energy (2x), Volthium
|
58
|
+
MatcherPattern(local_name=pattern, connectable=True)
|
59
|
+
for pattern in ("L-12V???AH-*", "LT-12V-*", "V-12V???Ah-*")
|
60
|
+
]
|
61
|
+
+ [ # Fliteboard, Electronix battery
|
62
|
+
{
|
63
|
+
"local_name": "libatt*",
|
64
|
+
"manufacturer_id": 21320,
|
65
|
+
"connectable": True,
|
66
|
+
},
|
67
|
+
{"local_name": "SV12V*", "manufacturer_id": 33384, "connectable": True},
|
68
|
+
{"local_name": "LT-24*", "manufacturer_id": 22618, "connectable": True},
|
69
|
+
]
|
70
|
+
+ [ # LiTime
|
71
|
+
MatcherPattern( # LiTime based on ser#
|
72
|
+
local_name="LT-12???BG-A0[0-6]*",
|
73
|
+
manufacturer_id=m_id,
|
74
|
+
connectable=True,
|
75
|
+
)
|
76
|
+
for m_id in (33384, 22618)
|
77
|
+
]
|
78
|
+
)
|
79
|
+
|
80
|
+
@staticmethod
|
81
|
+
def device_info() -> dict[str, str]:
|
82
|
+
"""Return device information for the battery management system."""
|
83
|
+
return {"manufacturer": "E&J Technology", "model": "Smart BMS"}
|
84
|
+
|
85
|
+
@staticmethod
|
86
|
+
def uuid_services() -> list[str]:
|
87
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
88
|
+
return ["6e400001-b5a3-f393-e0a9-e50e24dcca9e"]
|
89
|
+
|
90
|
+
@staticmethod
|
91
|
+
def uuid_rx() -> str:
|
92
|
+
"""Return 128-bit UUID of characteristic that provides notification/read property."""
|
93
|
+
return "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
|
94
|
+
|
95
|
+
@staticmethod
|
96
|
+
def uuid_tx() -> str:
|
97
|
+
"""Return 128-bit UUID of characteristic that provides write property."""
|
98
|
+
return "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
102
|
+
return frozenset(
|
103
|
+
{
|
104
|
+
"battery_charging",
|
105
|
+
"cycle_capacity",
|
106
|
+
"delta_voltage",
|
107
|
+
"power",
|
108
|
+
"runtime",
|
109
|
+
"voltage",
|
110
|
+
}
|
111
|
+
) # calculate further values from BMS provided set ones
|
112
|
+
|
113
|
+
def _notification_handler(
|
114
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
115
|
+
) -> None:
|
116
|
+
"""Handle the RX characteristics notify event (new data arrives)."""
|
117
|
+
|
118
|
+
if data.startswith(BMS._BT_MODULE_MSG):
|
119
|
+
self._log.debug("filtering AT cmd")
|
120
|
+
if not (data := data.removeprefix(BMS._BT_MODULE_MSG)):
|
121
|
+
return
|
122
|
+
|
123
|
+
if data.startswith(BMS._HEAD): # check for beginning of frame
|
124
|
+
self._data.clear()
|
125
|
+
|
126
|
+
self._data += data
|
127
|
+
|
128
|
+
self._log.debug(
|
129
|
+
"RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
|
130
|
+
)
|
131
|
+
|
132
|
+
exp_frame_len: Final[int] = (
|
133
|
+
int(self._data[7:11], 16)
|
134
|
+
if len(self._data) > 10
|
135
|
+
and all(chr(c) in hexdigits for c in self._data[7:11])
|
136
|
+
else 0xFFFF
|
137
|
+
)
|
138
|
+
|
139
|
+
if not self._data.startswith(BMS._HEAD) or (
|
140
|
+
not self._data.endswith(BMS._TAIL) and len(self._data) < exp_frame_len
|
141
|
+
):
|
142
|
+
return
|
143
|
+
|
144
|
+
if not self._data.endswith(BMS._TAIL):
|
145
|
+
self._log.debug("incorrect EOF: %s", data)
|
146
|
+
self._data.clear()
|
147
|
+
return
|
148
|
+
|
149
|
+
if not all(chr(c) in hexdigits for c in self._data[1:-1]):
|
150
|
+
self._log.debug("incorrect frame encoding.")
|
151
|
+
self._data.clear()
|
152
|
+
return
|
153
|
+
|
154
|
+
if len(self._data) != exp_frame_len:
|
155
|
+
self._log.debug(
|
156
|
+
"incorrect frame length %i != %i",
|
157
|
+
len(self._data),
|
158
|
+
exp_frame_len,
|
159
|
+
)
|
160
|
+
self._data.clear()
|
161
|
+
return
|
162
|
+
|
163
|
+
if not self.name.startswith(BMS._IGNORE_CRC) and (
|
164
|
+
crc := BMS._crc(self._data[1:-3])
|
165
|
+
) != int(self._data[-3:-1], 16):
|
166
|
+
# libattU firmware uses no CRC, so we ignore it
|
167
|
+
self._log.debug(
|
168
|
+
"invalid checksum 0x%X != 0x%X", int(self._data[-3:-1], 16), crc
|
169
|
+
)
|
170
|
+
self._data.clear()
|
171
|
+
return
|
172
|
+
|
173
|
+
self._log.debug(
|
174
|
+
"address: 0x%X, command 0x%X, version: 0x%X, length: 0x%X",
|
175
|
+
int(self._data[1:3], 16),
|
176
|
+
int(self._data[3:5], 16) & 0x7F,
|
177
|
+
int(self._data[5:7], 16),
|
178
|
+
len(self._data),
|
179
|
+
)
|
180
|
+
self._data_final = self._data.copy()
|
181
|
+
self._data_event.set()
|
182
|
+
|
183
|
+
@staticmethod
|
184
|
+
def _crc(data: bytearray) -> int:
|
185
|
+
return (sum(data) ^ 0xFF) & 0xFF
|
186
|
+
|
187
|
+
@staticmethod
|
188
|
+
def _cell_voltages(
|
189
|
+
data: bytearray,
|
190
|
+
*,
|
191
|
+
cells: int,
|
192
|
+
start: int,
|
193
|
+
size: int = 2,
|
194
|
+
byteorder: Literal["little", "big"] = "big",
|
195
|
+
divider: int = 1000,
|
196
|
+
) -> list[float]:
|
197
|
+
"""Return cell voltages from status message."""
|
198
|
+
return [
|
199
|
+
(value / divider)
|
200
|
+
for idx in range(cells)
|
201
|
+
if (value := int(data[start + size * idx : start + size * (idx + 1)], 16))
|
202
|
+
]
|
203
|
+
|
204
|
+
@staticmethod
|
205
|
+
def _conv_data(data: dict[int, bytearray]) -> BMSsample:
|
206
|
+
result: BMSsample = {}
|
207
|
+
for field in BMS._FIELDS:
|
208
|
+
result[field.key] = field.fct(
|
209
|
+
int(data[field.idx][field.pos : field.pos + field.size], 16)
|
210
|
+
)
|
211
|
+
return result
|
212
|
+
|
213
|
+
async def _async_update(self) -> BMSsample:
|
214
|
+
"""Update battery status information."""
|
215
|
+
raw_data: dict[int, bytearray] = {}
|
216
|
+
|
217
|
+
# query real-time information and capacity
|
218
|
+
for cmd in (b":000250000E03~", b":001031000E05~"):
|
219
|
+
await self._await_reply(cmd)
|
220
|
+
rsp: int = int(self._data_final[3:5], 16) & 0x7F
|
221
|
+
raw_data[rsp] = self._data_final
|
222
|
+
if rsp == Cmd.RT and len(self._data_final) == 0x8C:
|
223
|
+
# handle metrisun version
|
224
|
+
self._log.debug("single frame protocol detected")
|
225
|
+
raw_data[Cmd.CAP] = bytearray(15) + self._data_final[125:]
|
226
|
+
break
|
227
|
+
|
228
|
+
if len(raw_data) != len(list(Cmd)) or not all(
|
229
|
+
len(value) > 0 for value in raw_data.values()
|
230
|
+
):
|
231
|
+
return {}
|
232
|
+
|
233
|
+
return self._conv_data(raw_data) | {
|
234
|
+
"cell_voltages": BMS._cell_voltages(
|
235
|
+
raw_data[Cmd.RT], cells=BMS._MAX_CELLS, start=25, size=4
|
236
|
+
)
|
237
|
+
}
|
@@ -0,0 +1,143 @@
|
|
1
|
+
"""Module to support Felicity BMS.
|
2
|
+
|
3
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
4
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
5
|
+
"""
|
6
|
+
|
7
|
+
from collections.abc import Callable
|
8
|
+
from json import JSONDecodeError, loads
|
9
|
+
from typing import Any, Final
|
10
|
+
|
11
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
12
|
+
from bleak.backends.device import BLEDevice
|
13
|
+
from bleak.uuids import normalize_uuid_str
|
14
|
+
|
15
|
+
from aiobmsble import BMSsample, BMSvalue, MatcherPattern
|
16
|
+
from aiobmsble.basebms import BaseBMS
|
17
|
+
|
18
|
+
|
19
|
+
class BMS(BaseBMS):
|
20
|
+
"""Felicity BMS implementation."""
|
21
|
+
|
22
|
+
_HEAD: Final[bytes] = b"{"
|
23
|
+
_TAIL: Final[bytes] = b"}"
|
24
|
+
_CMD_PRE: Final[bytes] = b"wifilocalMonitor:" # CMD prefix
|
25
|
+
_CMD_BI: Final[bytes] = b"get dev basice infor"
|
26
|
+
_CMD_DT: Final[bytes] = b"get Date"
|
27
|
+
_CMD_RT: Final[bytes] = b"get dev real infor"
|
28
|
+
_FIELDS: Final[list[tuple[BMSvalue, str, Callable[[list], Any]]]] = [
|
29
|
+
("voltage", "Batt", lambda x: x[0][0] / 1000),
|
30
|
+
("current", "Batt", lambda x: x[1][0] / 10),
|
31
|
+
(
|
32
|
+
"cycle_charge",
|
33
|
+
"BatsocList",
|
34
|
+
lambda x: (int(x[0][0]) * int(x[0][2])) / 1e7,
|
35
|
+
),
|
36
|
+
("battery_level", "BatsocList", lambda x: x[0][0] / 100),
|
37
|
+
]
|
38
|
+
|
39
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
40
|
+
"""Initialize BMS."""
|
41
|
+
super().__init__(ble_device, reconnect)
|
42
|
+
self._data_final: dict = {}
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
46
|
+
"""Provide BluetoothMatcher definition."""
|
47
|
+
return [
|
48
|
+
{"local_name": pattern, "connectable": True} for pattern in ("F07*", "F10*")
|
49
|
+
]
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
def device_info() -> dict[str, str]:
|
53
|
+
"""Return device information for the battery management system."""
|
54
|
+
return {"manufacturer": "Felicity Solar", "model": "LiFePo4 battery"}
|
55
|
+
|
56
|
+
@staticmethod
|
57
|
+
def uuid_services() -> list[str]:
|
58
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
59
|
+
return [normalize_uuid_str("6e6f736a-4643-4d44-8fa9-0fafd005e455")]
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
def uuid_rx() -> str:
|
63
|
+
"""Return 128-bit UUID of characteristic that provides notification/read property."""
|
64
|
+
return "49535458-8341-43f4-a9d4-ec0e34729bb3"
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def uuid_tx() -> str:
|
68
|
+
"""Return 128-bit UUID of characteristic that provides write property."""
|
69
|
+
return "49535258-184d-4bd9-bc61-20c647249616"
|
70
|
+
|
71
|
+
@staticmethod
|
72
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
73
|
+
return frozenset(
|
74
|
+
{
|
75
|
+
"battery_charging",
|
76
|
+
"cycle_capacity",
|
77
|
+
"delta_voltage",
|
78
|
+
"power",
|
79
|
+
"runtime",
|
80
|
+
"temperature",
|
81
|
+
}
|
82
|
+
) # calculate further values from BMS provided set ones
|
83
|
+
|
84
|
+
def _notification_handler(
|
85
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
86
|
+
) -> None:
|
87
|
+
"""Handle the RX characteristics notify event (new data arrives)."""
|
88
|
+
|
89
|
+
if data.startswith(BMS._HEAD):
|
90
|
+
self._data = bytearray()
|
91
|
+
|
92
|
+
self._data += data
|
93
|
+
self._log.debug(
|
94
|
+
"RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
|
95
|
+
)
|
96
|
+
|
97
|
+
if not data.endswith(BMS._TAIL):
|
98
|
+
return
|
99
|
+
|
100
|
+
try:
|
101
|
+
self._data_final = loads(self._data)
|
102
|
+
except (JSONDecodeError, UnicodeDecodeError):
|
103
|
+
self._log.debug("JSON decode error: %s", self._data)
|
104
|
+
return
|
105
|
+
|
106
|
+
if (ver := self._data_final.get("CommVer", 0)) != 1:
|
107
|
+
self._log.debug("Unknown protocol version (%i)", ver)
|
108
|
+
return
|
109
|
+
|
110
|
+
self._data_event.set()
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def _conv_data(data: dict) -> BMSsample:
|
114
|
+
result: BMSsample = {}
|
115
|
+
for key, itm, func in BMS._FIELDS:
|
116
|
+
result[key] = func(data.get(itm, []))
|
117
|
+
return result
|
118
|
+
|
119
|
+
@staticmethod
|
120
|
+
def _conv_cells(data: dict) -> list[float]:
|
121
|
+
return [(value / 1000) for value in data.get("BatcelList", [])[0]]
|
122
|
+
|
123
|
+
@staticmethod
|
124
|
+
def _conv_temp(data: dict) -> list[float]:
|
125
|
+
return [
|
126
|
+
(value / 10) for value in data.get("BtemList", [])[0] if value != 0x7FFF
|
127
|
+
]
|
128
|
+
|
129
|
+
async def _async_update(self) -> BMSsample:
|
130
|
+
"""Update battery status information."""
|
131
|
+
|
132
|
+
await self._await_reply(BMS._CMD_PRE + BMS._CMD_RT)
|
133
|
+
|
134
|
+
return (
|
135
|
+
BMS._conv_data(self._data_final)
|
136
|
+
| {"temp_values": BMS._conv_temp(self._data_final)}
|
137
|
+
| {"cell_voltages": BMS._conv_cells(self._data_final)}
|
138
|
+
| {
|
139
|
+
"problem_code": int(
|
140
|
+
self._data_final.get("Bwarn", 0) + self._data_final.get("Bfault", 0)
|
141
|
+
)
|
142
|
+
}
|
143
|
+
)
|
aiobmsble/bms/jbd_bms.py
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
"""Module to support JBD Smart BMS.
|
2
|
+
|
3
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
4
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
5
|
+
"""
|
6
|
+
|
7
|
+
from typing import Final
|
8
|
+
|
9
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
10
|
+
from bleak.backends.device import BLEDevice
|
11
|
+
from bleak.uuids import normalize_uuid_str
|
12
|
+
|
13
|
+
from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
|
14
|
+
from aiobmsble.basebms import BaseBMS
|
15
|
+
|
16
|
+
|
17
|
+
class BMS(BaseBMS):
|
18
|
+
"""JBD Smart BMS class implementation."""
|
19
|
+
|
20
|
+
HEAD_RSP: Final[bytes] = bytes([0xDD]) # header for responses
|
21
|
+
HEAD_CMD: Final[bytes] = bytes([0xDD, 0xA5]) # read header for commands
|
22
|
+
TAIL: Final[int] = 0x77 # tail for command
|
23
|
+
INFO_LEN: Final[int] = 7 # minimum frame size
|
24
|
+
BASIC_INFO: Final[int] = 23 # basic info data length
|
25
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
26
|
+
BMSdp("temp_sensors", 26, 1, False, lambda x: x), # count is not limited
|
27
|
+
BMSdp("voltage", 4, 2, False, lambda x: x / 100),
|
28
|
+
BMSdp("current", 6, 2, True, lambda x: x / 100),
|
29
|
+
BMSdp("battery_level", 23, 1, False, lambda x: x),
|
30
|
+
BMSdp("cycle_charge", 8, 2, False, lambda x: x / 100),
|
31
|
+
BMSdp("cycles", 12, 2, False, lambda x: x),
|
32
|
+
BMSdp("problem_code", 20, 2, False, lambda x: x),
|
33
|
+
) # general protocol v4
|
34
|
+
|
35
|
+
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
|
36
|
+
"""Intialize private BMS members."""
|
37
|
+
super().__init__(ble_device, reconnect)
|
38
|
+
self._valid_reply: int = 0x00
|
39
|
+
self._data_final: bytearray = bytearray()
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
43
|
+
"""Provide BluetoothMatcher definition."""
|
44
|
+
return [
|
45
|
+
MatcherPattern(
|
46
|
+
local_name=pattern,
|
47
|
+
service_uuid=BMS.uuid_services()[0],
|
48
|
+
connectable=True,
|
49
|
+
)
|
50
|
+
for pattern in (
|
51
|
+
"JBD-*",
|
52
|
+
"SP0?S*",
|
53
|
+
"SP1?S*",
|
54
|
+
"SP2?S*",
|
55
|
+
"AP2?S*",
|
56
|
+
"GJ-*", # accurat batteries
|
57
|
+
"SX1*", # Supervolt v3
|
58
|
+
"DP04S*", # ECO-WORTHY, DCHOUSE
|
59
|
+
"ECO-LFP*", # ECO-WORTHY rack (use m_id?)
|
60
|
+
"121?0*", # Eleksol, Ultimatron
|
61
|
+
"12200*",
|
62
|
+
"12300*",
|
63
|
+
"SBL-*", # SBL
|
64
|
+
"LT40AH", # LionTron
|
65
|
+
"PKT*", # Perfektium
|
66
|
+
"gokwh*",
|
67
|
+
"OGR-*", # OGRPHY
|
68
|
+
"DWC*", # Vatrer
|
69
|
+
"DXD*", # Vatrer
|
70
|
+
"xiaoxiang*", # xiaoxiang BMS
|
71
|
+
"AL12-*", # Aolithium
|
72
|
+
"BS20*", # BasenGreen
|
73
|
+
"BT LP*", # LANPWR
|
74
|
+
)
|
75
|
+
] + [
|
76
|
+
MatcherPattern(
|
77
|
+
service_uuid=BMS.uuid_services()[0],
|
78
|
+
manufacturer_id=m_id,
|
79
|
+
connectable=True,
|
80
|
+
)
|
81
|
+
for m_id in (0x0211, 0x3E70, 0xC1A4)
|
82
|
+
# Liontron, LISMART1240LX/LISMART1255LX,
|
83
|
+
# LionTron XL19110253 / EPOCH batteries 12.8V 460Ah - 12460A-H
|
84
|
+
]
|
85
|
+
|
86
|
+
@staticmethod
|
87
|
+
def device_info() -> dict[str, str]:
|
88
|
+
"""Return device information for the battery management system."""
|
89
|
+
return {"manufacturer": "Jiabaida", "model": "Smart BMS"}
|
90
|
+
|
91
|
+
@staticmethod
|
92
|
+
def uuid_services() -> list[str]:
|
93
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
94
|
+
return [normalize_uuid_str("ff00")]
|
95
|
+
|
96
|
+
@staticmethod
|
97
|
+
def uuid_rx() -> str:
|
98
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
99
|
+
return "ff01"
|
100
|
+
|
101
|
+
@staticmethod
|
102
|
+
def uuid_tx() -> str:
|
103
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
104
|
+
return "ff02"
|
105
|
+
|
106
|
+
@staticmethod
|
107
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
108
|
+
return frozenset(
|
109
|
+
{
|
110
|
+
"power",
|
111
|
+
"battery_charging",
|
112
|
+
"cycle_capacity",
|
113
|
+
"runtime",
|
114
|
+
"delta_voltage",
|
115
|
+
"temperature",
|
116
|
+
}
|
117
|
+
)
|
118
|
+
|
119
|
+
def _notification_handler(
|
120
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
121
|
+
) -> None:
|
122
|
+
# check if answer is a heading of basic info (0x3) or cell block info (0x4)
|
123
|
+
if (
|
124
|
+
data.startswith(self.HEAD_RSP)
|
125
|
+
and len(self._data) > self.INFO_LEN
|
126
|
+
and data[1] in (0x03, 0x04)
|
127
|
+
and data[2] == 0x00
|
128
|
+
and len(self._data) >= self.INFO_LEN + self._data[3]
|
129
|
+
):
|
130
|
+
self._data = bytearray()
|
131
|
+
|
132
|
+
self._data += data
|
133
|
+
self._log.debug(
|
134
|
+
"RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
|
135
|
+
)
|
136
|
+
|
137
|
+
# verify that data is long enough
|
138
|
+
if (
|
139
|
+
len(self._data) < BMS.INFO_LEN
|
140
|
+
or len(self._data) < BMS.INFO_LEN + self._data[3]
|
141
|
+
):
|
142
|
+
return
|
143
|
+
|
144
|
+
# check correct frame ending
|
145
|
+
frame_end: Final[int] = BMS.INFO_LEN + self._data[3] - 1
|
146
|
+
if self._data[frame_end] != BMS.TAIL:
|
147
|
+
self._log.debug("incorrect frame end (length: %i).", len(self._data))
|
148
|
+
return
|
149
|
+
|
150
|
+
if (crc := BMS._crc(self._data[2 : frame_end - 2])) != int.from_bytes(
|
151
|
+
self._data[frame_end - 2 : frame_end], "big"
|
152
|
+
):
|
153
|
+
self._log.debug(
|
154
|
+
"invalid checksum 0x%X != 0x%X",
|
155
|
+
int.from_bytes(self._data[frame_end - 2 : frame_end], "big"),
|
156
|
+
crc,
|
157
|
+
)
|
158
|
+
return
|
159
|
+
|
160
|
+
if len(self._data) != BMS.INFO_LEN + self._data[3]:
|
161
|
+
self._log.debug("wrong data length (%i): %s", len(self._data), self._data)
|
162
|
+
|
163
|
+
if self._data[1] != self._valid_reply:
|
164
|
+
self._log.debug("unexpected response (type 0x%X)", self._data[1])
|
165
|
+
return
|
166
|
+
|
167
|
+
self._data_final = self._data
|
168
|
+
self._data_event.set()
|
169
|
+
|
170
|
+
@staticmethod
|
171
|
+
def _crc(frame: bytearray) -> int:
|
172
|
+
"""Calculate JBD frame CRC."""
|
173
|
+
return 0x10000 - sum(frame)
|
174
|
+
|
175
|
+
@staticmethod
|
176
|
+
def _cmd(cmd: bytes) -> bytes:
|
177
|
+
"""Assemble a JBD BMS command."""
|
178
|
+
frame = bytearray([*BMS.HEAD_CMD, cmd[0], 0x00])
|
179
|
+
frame.extend([*BMS._crc(frame[2:4]).to_bytes(2, "big"), BMS.TAIL])
|
180
|
+
return bytes(frame)
|
181
|
+
|
182
|
+
async def _await_cmd_resp(self, cmd: int) -> None:
|
183
|
+
msg: Final[bytes] = BMS._cmd(bytes([cmd]))
|
184
|
+
self._valid_reply = msg[2]
|
185
|
+
await self._await_reply(msg)
|
186
|
+
self._valid_reply = 0x00
|
187
|
+
|
188
|
+
async def _async_update(self) -> BMSsample:
|
189
|
+
"""Update battery status information."""
|
190
|
+
data: BMSsample = {}
|
191
|
+
await self._await_cmd_resp(0x03)
|
192
|
+
data = BMS._decode_data(BMS._FIELDS, self._data_final)
|
193
|
+
data["temp_values"] = BMS._temp_values(
|
194
|
+
self._data_final,
|
195
|
+
values=data.get("temp_sensors", 0),
|
196
|
+
start=27,
|
197
|
+
signed=False,
|
198
|
+
offset=2731,
|
199
|
+
divider=10,
|
200
|
+
)
|
201
|
+
|
202
|
+
await self._await_cmd_resp(0x04)
|
203
|
+
data["cell_voltages"] = BMS._cell_voltages(
|
204
|
+
self._data_final, cells=self._data_final[3] // 2, start=4, byteorder="big"
|
205
|
+
)
|
206
|
+
|
207
|
+
return data
|