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.
Files changed (37) hide show
  1. aiobmsble/__init__.py +5 -1
  2. aiobmsble/__main__.py +5 -1
  3. aiobmsble/basebms.py +10 -1
  4. aiobmsble/bms/__init__.py +5 -0
  5. aiobmsble/bms/abc_bms.py +168 -0
  6. aiobmsble/bms/ant_bms.py +200 -0
  7. aiobmsble/bms/braunpwr_bms.py +171 -0
  8. aiobmsble/bms/cbtpwr_bms.py +172 -0
  9. aiobmsble/bms/cbtpwr_vb_bms.py +188 -0
  10. aiobmsble/bms/daly_bms.py +168 -0
  11. aiobmsble/bms/dpwrcore_bms.py +211 -0
  12. aiobmsble/bms/dummy_bms.py +93 -0
  13. aiobmsble/bms/ecoworthy_bms.py +155 -0
  14. aiobmsble/bms/ective_bms.py +181 -0
  15. aiobmsble/bms/ej_bms.py +237 -0
  16. aiobmsble/bms/felicity_bms.py +143 -0
  17. aiobmsble/bms/jbd_bms.py +207 -0
  18. aiobmsble/bms/jikong_bms.py +305 -0
  19. aiobmsble/bms/neey_bms.py +218 -0
  20. aiobmsble/bms/ogt_bms.py +218 -0
  21. aiobmsble/bms/pro_bms.py +148 -0
  22. aiobmsble/bms/redodo_bms.py +131 -0
  23. aiobmsble/bms/renogy_bms.py +152 -0
  24. aiobmsble/bms/renogy_pro_bms.py +109 -0
  25. aiobmsble/bms/roypow_bms.py +190 -0
  26. aiobmsble/bms/seplos_bms.py +249 -0
  27. aiobmsble/bms/seplos_v2_bms.py +209 -0
  28. aiobmsble/bms/tdt_bms.py +203 -0
  29. aiobmsble/bms/tianpwr_bms.py +142 -0
  30. aiobmsble/utils.py +16 -6
  31. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/METADATA +3 -2
  32. aiobmsble-0.2.2.dist-info/RECORD +36 -0
  33. aiobmsble-0.2.0.dist-info/RECORD +0 -10
  34. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/WHEEL +0 -0
  35. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/entry_points.txt +0 -0
  36. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/licenses/LICENSE +0 -0
  37. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,209 @@
1
+ """Module to support Seplos v2 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, crc_xmodem
15
+
16
+
17
+ class BMS(BaseBMS):
18
+ """Seplos v2 BMS implementation."""
19
+
20
+ _HEAD: Final[bytes] = b"\x7e"
21
+ _TAIL: Final[bytes] = b"\x0d"
22
+ _CMD_VER: Final[int] = 0x10 # TX protocol version
23
+ _RSP_VER: Final[int] = 0x14 # RX protocol version
24
+ _MIN_LEN: Final[int] = 10
25
+ _MAX_SUBS: Final[int] = 0xF
26
+ _CELL_POS: Final[int] = 9
27
+ _PRB_MAX: Final[int] = 8 # max number of alarm event bytes
28
+ _PRB_MASK: Final[int] = ~0x82FFFF # ignore byte 7-8 + byte 6 (bit 7,2)
29
+ _PFIELDS: Final[tuple[BMSdp, ...]] = ( # Seplos V2: single machine data
30
+ BMSdp("voltage", 2, 2, False, lambda x: x / 100),
31
+ BMSdp("current", 0, 2, True, lambda x: x / 100), # /10 for 0x62
32
+ BMSdp("cycle_charge", 4, 2, False, lambda x: x / 100), # /10 for 0x62
33
+ BMSdp("cycles", 13, 2, False, lambda x: x),
34
+ BMSdp("battery_level", 9, 2, False, lambda x: x / 10),
35
+ )
36
+ _GSMD_LEN: Final[int] = _CELL_POS + max((dp.pos + dp.size) for dp in _PFIELDS) + 3
37
+ _CMDS: Final[list[tuple[int, bytes]]] = [(0x51, b""), (0x61, b"\x00"), (0x62, b"")]
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[int, bytearray] = {}
43
+ self._exp_len: int = BMS._MIN_LEN
44
+ self._exp_reply: set[int] = set()
45
+
46
+ @staticmethod
47
+ def matcher_dict_list() -> list[MatcherPattern]:
48
+ """Provide BluetoothMatcher definition."""
49
+ return [
50
+ {
51
+ "local_name": pattern,
52
+ "service_uuid": BMS.uuid_services()[0],
53
+ "connectable": True,
54
+ }
55
+ for pattern in ("BP0?", "BP1?", "BP2?")
56
+ ]
57
+
58
+ @staticmethod
59
+ def device_info() -> dict[str, str]:
60
+ """Return device information for the battery management system."""
61
+ return {"manufacturer": "Seplos", "model": "Smart BMS V2"}
62
+
63
+ @staticmethod
64
+ def uuid_services() -> list[str]:
65
+ """Return list of 128-bit UUIDs of services required by BMS."""
66
+ return [normalize_uuid_str("ff00")]
67
+
68
+ @staticmethod
69
+ def uuid_rx() -> str:
70
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
71
+ return "ff01"
72
+
73
+ @staticmethod
74
+ def uuid_tx() -> str:
75
+ """Return 16-bit UUID of characteristic that provides write property."""
76
+ return "ff02"
77
+
78
+ @staticmethod
79
+ def _calc_values() -> frozenset[BMSvalue]:
80
+ return frozenset(
81
+ {
82
+ "battery_charging",
83
+ "cycle_capacity",
84
+ "delta_voltage",
85
+ "power",
86
+ "runtime",
87
+ "temperature",
88
+ }
89
+ ) # calculate further values from BMS provided set ones
90
+
91
+ def _notification_handler(
92
+ self, _sender: BleakGATTCharacteristic, data: bytearray
93
+ ) -> None:
94
+ """Handle the RX characteristics notify event (new data arrives)."""
95
+ if (
96
+ len(data) > BMS._MIN_LEN
97
+ and data.startswith(BMS._HEAD)
98
+ and len(self._data) >= self._exp_len
99
+ ):
100
+ self._exp_len = BMS._MIN_LEN + int.from_bytes(data[5:7])
101
+ self._data = bytearray()
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
+ # verify that data is long enough
109
+ if len(self._data) < self._exp_len:
110
+ return
111
+
112
+ if not self._data.endswith(BMS._TAIL):
113
+ self._log.debug("incorrect frame end: %s", self._data)
114
+ return
115
+
116
+ if self._data[1] != BMS._RSP_VER:
117
+ self._log.debug("unknown frame version: V%.1f", self._data[1] / 10)
118
+ return
119
+
120
+ if self._data[4]:
121
+ self._log.debug("BMS reported error code: 0x%X", self._data[4])
122
+ return
123
+
124
+ if (crc := crc_xmodem(self._data[1:-3])) != int.from_bytes(self._data[-3:-1]):
125
+ self._log.debug(
126
+ "invalid checksum 0x%X != 0x%X",
127
+ crc,
128
+ int.from_bytes(self._data[-3:-1]),
129
+ )
130
+ return
131
+
132
+ self._log.debug(
133
+ "address: 0x%X, function: 0x%X, return: 0x%X",
134
+ self._data[2],
135
+ self._data[3],
136
+ self._data[4],
137
+ )
138
+
139
+ self._data_final[self._data[3]] = self._data
140
+ try:
141
+ self._exp_reply.remove(self._data[3])
142
+ self._data_event.set()
143
+ except KeyError:
144
+ self._log.debug("unexpected reply: 0x%X", self._data[3])
145
+
146
+ async def _init_connection(
147
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
148
+ ) -> None:
149
+ """Initialize protocol state."""
150
+ await super()._init_connection()
151
+ self._exp_len = BMS._MIN_LEN
152
+
153
+ @staticmethod
154
+ def _cmd(cmd: int, address: int = 0, data: bytearray = bytearray()) -> bytes:
155
+ """Assemble a Seplos V2 BMS command."""
156
+ assert cmd in (0x47, 0x51, 0x61, 0x62, 0x04) # allow only read commands
157
+ frame = bytearray([*BMS._HEAD, BMS._CMD_VER, address, 0x46, cmd])
158
+ frame += len(data).to_bytes(2, "big", signed=False) + data
159
+ frame += int.to_bytes(crc_xmodem(frame[1:]), 2, byteorder="big") + BMS._TAIL
160
+ return bytes(frame)
161
+
162
+ async def _async_update(self) -> BMSsample:
163
+ """Update battery status information."""
164
+
165
+ for cmd, data in BMS._CMDS:
166
+ self._exp_reply.add(cmd)
167
+ await self._await_reply(BMS._cmd(cmd, data=bytearray(data)))
168
+
169
+ result: BMSsample = {}
170
+ result["cell_count"] = self._data_final[0x61][BMS._CELL_POS]
171
+ result["temp_sensors"] = self._data_final[0x61][
172
+ BMS._CELL_POS + result["cell_count"] * 2 + 1
173
+ ]
174
+ ct_blk_len: Final[int] = (result["cell_count"] + result["temp_sensors"]) * 2 + 2
175
+
176
+ if (BMS._GSMD_LEN + ct_blk_len) > len(self._data_final[0x61]):
177
+ raise ValueError("message too short to decode data")
178
+
179
+ result |= BMS._decode_data(
180
+ BMS._PFIELDS, self._data_final[0x61], offset=BMS._CELL_POS + ct_blk_len
181
+ )
182
+
183
+ # get extention pack count from parallel data (main pack)
184
+ result["pack_count"] = self._data_final[0x51][42]
185
+
186
+ # get alarms from parallel data (main pack)
187
+ alarm_evt: Final[int] = min(self._data_final[0x62][46], BMS._PRB_MAX)
188
+ result["problem_code"] = (
189
+ int.from_bytes(self._data_final[0x62][47 : 47 + alarm_evt], byteorder="big")
190
+ & BMS._PRB_MASK
191
+ )
192
+
193
+ result["cell_voltages"] = BMS._cell_voltages(
194
+ self._data_final[0x61],
195
+ cells=self._data_final[0x61][BMS._CELL_POS],
196
+ start=10,
197
+ )
198
+ result["temp_values"] = BMS._temp_values(
199
+ self._data_final[0x61],
200
+ values=result["temp_sensors"],
201
+ start=BMS._CELL_POS + result.get("cell_count", 0) * 2 + 2,
202
+ signed=False,
203
+ offset=2731,
204
+ divider=10,
205
+ )
206
+
207
+ self._data_final.clear()
208
+
209
+ return result
@@ -0,0 +1,203 @@
1
+ """Module to support TDT 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, crc_modbus
15
+
16
+
17
+ class BMS(BaseBMS):
18
+ """TDT BMS implementation."""
19
+
20
+ _UUID_CFG: Final[str] = "fffa"
21
+ _HEAD: Final[int] = 0x7E
22
+ _CMD_HEADS: list[int] = [0x7E, 0x1E] # alternative command head
23
+ _TAIL: Final[int] = 0x0D
24
+ _CMD_VER: Final[int] = 0x00
25
+ _RSP_VER: Final[frozenset[int]] = frozenset({0x00, 0x04})
26
+ _CELL_POS: Final[int] = 0x8
27
+ _INFO_LEN: Final[int] = 10 # minimal frame length
28
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
29
+ BMSdp("voltage", 2, 2, False, lambda x: x / 100, 0x8C),
30
+ BMSdp(
31
+ "current",
32
+ 0,
33
+ 2,
34
+ False,
35
+ lambda x: (x & 0x3FFF) / 10 * (-1 if x >> 15 else 1),
36
+ 0x8C,
37
+ ),
38
+ BMSdp("cycle_charge", 4, 2, False, lambda x: x / 10, 0x8C),
39
+ BMSdp("battery_level", 13, 1, False, lambda x: x, 0x8C),
40
+ BMSdp("cycles", 8, 2, False, lambda x: x, 0x8C),
41
+ ) # problem code is not included in the list, but extra
42
+ _CMDS: Final[list[int]] = [*list({field.idx for field in _FIELDS}), 0x8D]
43
+
44
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
45
+ """Initialize BMS."""
46
+ super().__init__(ble_device, reconnect)
47
+ self._data_final: dict[int, bytearray] = {}
48
+ self._cmd_heads: list[int] = BMS._CMD_HEADS
49
+ self._exp_len: int = 0
50
+
51
+ @staticmethod
52
+ def matcher_dict_list() -> list[MatcherPattern]:
53
+ """Provide BluetoothMatcher definition."""
54
+ return [{"manufacturer_id": 54976, "connectable": True}]
55
+
56
+ @staticmethod
57
+ def device_info() -> dict[str, str]:
58
+ """Return device information for the battery management system."""
59
+ return {"manufacturer": "TDT", "model": "Smart BMS"}
60
+
61
+ @staticmethod
62
+ def uuid_services() -> list[str]:
63
+ """Return list of 128-bit UUIDs of services required by BMS."""
64
+ return [normalize_uuid_str("fff0")]
65
+
66
+ @staticmethod
67
+ def uuid_rx() -> str:
68
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
69
+ return "fff1"
70
+
71
+ @staticmethod
72
+ def uuid_tx() -> str:
73
+ """Return 16-bit UUID of characteristic that provides write property."""
74
+ return "fff2"
75
+
76
+ @staticmethod
77
+ def _calc_values() -> frozenset[BMSvalue]:
78
+ return frozenset(
79
+ {
80
+ "battery_charging",
81
+ "cycle_capacity",
82
+ "delta_voltage",
83
+ "power",
84
+ "runtime",
85
+ "temperature",
86
+ }
87
+ ) # calculate further values from BMS provided set ones
88
+
89
+ async def _init_connection(
90
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
91
+ ) -> None:
92
+ await self._await_reply(
93
+ data=b"HiLink", char=BMS._UUID_CFG, wait_for_notify=False
94
+ )
95
+ if (
96
+ ret := int.from_bytes(await self._client.read_gatt_char(BMS._UUID_CFG))
97
+ ) != 0x1:
98
+ self._log.debug("error unlocking BMS: %X", ret)
99
+
100
+ await super()._init_connection()
101
+
102
+ def _notification_handler(
103
+ self, _sender: BleakGATTCharacteristic, data: bytearray
104
+ ) -> None:
105
+ """Handle the RX characteristics notify event (new data arrives)."""
106
+ self._log.debug("RX BLE data: %s", data)
107
+
108
+ if (
109
+ len(data) > BMS._INFO_LEN
110
+ and data[0] == BMS._HEAD
111
+ and len(self._data) >= self._exp_len
112
+ ):
113
+ self._exp_len = BMS._INFO_LEN + int.from_bytes(data[6:8])
114
+ self._data = bytearray()
115
+
116
+ self._data += data
117
+ self._log.debug(
118
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
119
+ )
120
+
121
+ # verify that data is long enough
122
+ if len(self._data) < max(BMS._INFO_LEN, self._exp_len):
123
+ return
124
+
125
+ if self._data[-1] != BMS._TAIL:
126
+ self._log.debug("frame end incorrect: %s", self._data)
127
+ return
128
+
129
+ if self._data[1] not in BMS._RSP_VER:
130
+ self._log.debug("unknown frame version: V%.1f", self._data[1] / 10)
131
+ return
132
+
133
+ if self._data[4]:
134
+ self._log.debug("BMS reported error code: 0x%X", self._data[4])
135
+ return
136
+
137
+ if (crc := crc_modbus(self._data[:-3])) != int.from_bytes(
138
+ self._data[-3:-1], "big"
139
+ ):
140
+ self._log.debug(
141
+ "invalid checksum 0x%X != 0x%X",
142
+ int.from_bytes(self._data[-3:-1], "big"),
143
+ crc,
144
+ )
145
+ return
146
+ self._data_final[self._data[5]] = self._data
147
+ self._data_event.set()
148
+
149
+ @staticmethod
150
+ def _cmd(cmd: int, data: bytearray = bytearray(), cmd_head: int = _HEAD) -> bytes:
151
+ """Assemble a TDT BMS command."""
152
+ assert cmd in (0x8C, 0x8D, 0x92) # allow only read commands
153
+
154
+ frame = bytearray([cmd_head, BMS._CMD_VER, 0x1, 0x3, 0x0, cmd])
155
+ frame += len(data).to_bytes(2, "big", signed=False) + data
156
+ frame += crc_modbus(frame).to_bytes(2, "big") + bytes([BMS._TAIL])
157
+
158
+ return bytes(frame)
159
+
160
+ async def _async_update(self) -> BMSsample:
161
+ """Update battery status information."""
162
+
163
+ for head in self._cmd_heads:
164
+ try:
165
+ for cmd in BMS._CMDS:
166
+ await self._await_reply(BMS._cmd(cmd, cmd_head=head))
167
+ self._cmd_heads = [head] # set to single head for further commands
168
+ break
169
+ except TimeoutError:
170
+ ... # try next command head
171
+ else:
172
+ raise TimeoutError
173
+
174
+ result: BMSsample = {"cell_count": self._data_final[0x8C][BMS._CELL_POS]}
175
+ result["temp_sensors"] = self._data_final[0x8C][
176
+ BMS._CELL_POS + result["cell_count"] * 2 + 1
177
+ ]
178
+
179
+ result["cell_voltages"] = BMS._cell_voltages(
180
+ self._data_final[0x8C],
181
+ cells=result.get("cell_count", 0),
182
+ start=BMS._CELL_POS + 1,
183
+ )
184
+ result["temp_values"] = BMS._temp_values(
185
+ self._data_final[0x8C],
186
+ values=result["temp_sensors"],
187
+ start=BMS._CELL_POS + result.get("cell_count", 0) * 2 + 2,
188
+ signed=False,
189
+ offset=2731,
190
+ divider=10,
191
+ )
192
+ idx: Final[int] = result.get("cell_count", 0) + result.get("temp_sensors", 0)
193
+
194
+ result |= BMS._decode_data(
195
+ BMS._FIELDS, self._data_final, offset=BMS._CELL_POS + idx * 2 + 2
196
+ )
197
+ result["problem_code"] = int.from_bytes(
198
+ self._data_final[0x8D][BMS._CELL_POS + idx + 6 : BMS._CELL_POS + idx + 8]
199
+ )
200
+
201
+ self._data_final.clear()
202
+
203
+ return result
@@ -0,0 +1,142 @@
1
+ """Module to support TianPwr 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
+ """TianPwr BMS implementation."""
19
+
20
+ _HEAD: Final[bytes] = b"\x55"
21
+ _TAIL: Final[bytes] = b"\xaa"
22
+ _RDCMD: Final[bytes] = b"\x04"
23
+ _MAX_CELLS: Final[int] = 16
24
+ _MAX_TEMP: Final[int] = 6
25
+ _MIN_LEN: Final[int] = 4
26
+ _DEF_LEN: Final[int] = 20
27
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
28
+ BMSdp("battery_level", 3, 2, False, lambda x: x, 0x83),
29
+ BMSdp("voltage", 5, 2, False, lambda x: x / 100, 0x83),
30
+ BMSdp("current", 13, 2, True, lambda x: x / 100, 0x83),
31
+ BMSdp("problem_code", 11, 8, False, lambda x: x, 0x84),
32
+ BMSdp("cell_count", 3, 1, False, lambda x: x, 0x84),
33
+ BMSdp("temp_sensors", 4, 1, False, lambda x: x, 0x84),
34
+ BMSdp("design_capacity", 5, 2, False, lambda x: x // 100, 0x84),
35
+ BMSdp("cycle_charge", 7, 2, False, lambda x: x / 100, 0x84),
36
+ BMSdp("cycles", 9, 2, False, lambda x: x, 0x84),
37
+ )
38
+ _CMDS: Final[set[int]] = set({field.idx for field in _FIELDS}) | set({0x87})
39
+
40
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
41
+ """Initialize BMS."""
42
+ super().__init__(ble_device, reconnect)
43
+ self._data_final: dict[int, bytearray] = {}
44
+
45
+ @staticmethod
46
+ def matcher_dict_list() -> list[MatcherPattern]:
47
+ """Provide BluetoothMatcher definition."""
48
+ return [{"local_name": "TP_*", "connectable": True}]
49
+
50
+ @staticmethod
51
+ def device_info() -> dict[str, str]:
52
+ """Return device information for the battery management system."""
53
+ return {"manufacturer": "TianPwr", "model": "SmartBMS"}
54
+
55
+ @staticmethod
56
+ def uuid_services() -> list[str]:
57
+ """Return list of 128-bit UUIDs of services required by BMS."""
58
+ return [normalize_uuid_str("ff00")]
59
+
60
+ @staticmethod
61
+ def uuid_rx() -> str:
62
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
63
+ return "ff01"
64
+
65
+ @staticmethod
66
+ def uuid_tx() -> str:
67
+ """Return 16-bit UUID of characteristic that provides write property."""
68
+ return "ff02"
69
+
70
+ @staticmethod
71
+ def _calc_values() -> frozenset[BMSvalue]:
72
+ return frozenset(
73
+ {
74
+ "battery_charging",
75
+ "cycle_capacity",
76
+ "delta_voltage",
77
+ "power",
78
+ "temperature",
79
+ }
80
+ ) # calculate further values from BMS provided set ones
81
+
82
+ def _notification_handler(
83
+ self, _sender: BleakGATTCharacteristic, data: bytearray
84
+ ) -> None:
85
+ """Handle the RX characteristics notify event (new data arrives)."""
86
+ self._log.debug("RX BLE data: %s", data)
87
+
88
+ # verify that data is long enough
89
+ if len(data) != BMS._DEF_LEN:
90
+ self._log.debug("incorrect frame length")
91
+ return
92
+
93
+ if not data.startswith(BMS._HEAD):
94
+ self._log.debug("incorrect SOF.")
95
+ return
96
+
97
+ if not data.endswith(BMS._TAIL):
98
+ self._log.debug("incorrect EOF.")
99
+ return
100
+
101
+ self._data_final[data[2]] = data.copy()
102
+ self._data_event.set()
103
+
104
+ @staticmethod
105
+ def _cmd(addr: int) -> bytes:
106
+ """Assemble a TianPwr BMS command."""
107
+ return BMS._HEAD + BMS._RDCMD + addr.to_bytes(1) + BMS._TAIL
108
+
109
+ async def _async_update(self) -> BMSsample:
110
+ """Update battery status information."""
111
+
112
+ self._data_final.clear()
113
+ for cmd in BMS._CMDS:
114
+ await self._await_reply(BMS._cmd(cmd))
115
+
116
+ result: BMSsample = BMS._decode_data(BMS._FIELDS, self._data_final)
117
+
118
+ for cmd in range(
119
+ 0x88, 0x89 + min(result.get("cell_count", 0), BMS._MAX_CELLS) // 8
120
+ ):
121
+ await self._await_reply(BMS._cmd(cmd))
122
+ result["cell_voltages"] = result.setdefault(
123
+ "cell_voltages", []
124
+ ) + BMS._cell_voltages(
125
+ self._data_final.get(cmd, bytearray()), cells=8, start=3
126
+ )
127
+
128
+ if {0x83, 0x87}.issubset(self._data_final):
129
+ result["temp_values"] = [
130
+ int.from_bytes(
131
+ self._data_final[0x83][idx : idx + 2], byteorder="big", signed=True
132
+ )
133
+ / 10
134
+ for idx in (7, 11) # take ambient and mosfet temperature
135
+ ] + BMS._temp_values(
136
+ self._data_final.get(0x87, bytearray()),
137
+ values=min(BMS._MAX_TEMP, result.get("temp_sensors", 0)),
138
+ start=3,
139
+ divider=10,
140
+ )
141
+
142
+ return result
aiobmsble/utils.py CHANGED
@@ -1,4 +1,8 @@
1
- """Utilitiy/Support functions for aiobmsble."""
1
+ """Utilitiy/Support functions for aiobmsble.
2
+
3
+ Project: aiobmsble, https://pypi.org/p/aiobmsble/
4
+ License: Apache-2.0, http://www.apache.org/licenses/
5
+ """
2
6
 
3
7
  from fnmatch import translate
4
8
  from functools import lru_cache
@@ -6,11 +10,15 @@ import importlib
6
10
  import pkgutil
7
11
  import re
8
12
  from types import ModuleType
13
+ from typing import Final
9
14
 
10
15
  from bleak.backends.scanner import AdvertisementData
11
16
 
12
17
  from aiobmsble import MatcherPattern
13
18
  from aiobmsble.basebms import BaseBMS
19
+ import aiobmsble.bms
20
+
21
+ _MODULE_POSTFIX: Final[str] = "_bms"
14
22
 
15
23
 
16
24
  def _advertisement_matches(
@@ -20,7 +28,7 @@ def _advertisement_matches(
20
28
  """Determine whether the given advertisement data matches the specified pattern.
21
29
 
22
30
  Args:
23
- matcher (AdvertisementPattern): A dictionary containing the matching criteria.
31
+ matcher (MatcherPattern): A dictionary containing the matching criteria.
24
32
  Possible keys include:
25
33
  - "service_uuid" (str): A specific service 128-bit UUID to match.
26
34
  - "service_data_uuid" (str): A specific service data UUID to match.
@@ -78,8 +86,8 @@ def load_bms_plugins() -> set[ModuleType]:
78
86
  """
79
87
  return {
80
88
  importlib.import_module(f"aiobmsble.bms.{module_name}")
81
- for _, module_name, _ in pkgutil.iter_modules(["aiobmsble/bms"])
82
- if module_name.endswith("_bms")
89
+ for _, module_name, _ in pkgutil.iter_modules(aiobmsble.bms.__path__)
90
+ if module_name.endswith(_MODULE_POSTFIX)
83
91
  }
84
92
 
85
93
 
@@ -87,14 +95,16 @@ def bms_cls(name: str) -> type[BaseBMS] | None:
87
95
  """Return the BMS class that is defined by the name argument.
88
96
 
89
97
  Args:
90
- name (str): The name of the BMS type
98
+ name (str): The name of the BMS type (filename of the module)
91
99
 
92
100
  Returns:
93
101
  type[BaseBMS] | None: If the BMS class defined by name is found, None otherwise.
94
102
 
95
103
  """
104
+ if not name.endswith(_MODULE_POSTFIX):
105
+ return None
96
106
  try:
97
- bms_module: ModuleType = importlib.import_module(f"aiobmsble.bms.{name}_bms")
107
+ bms_module: ModuleType = importlib.import_module(f"aiobmsble.bms.{name}")
98
108
  except ModuleNotFoundError:
99
109
  return None
100
110
  return bms_module.BMS
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiobmsble
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Asynchronous Python library to query battery management systems via Bluetooth Low Energy.
5
5
  Author: Patrick Loschmidt
6
6
  Maintainer: Patrick Loschmidt
@@ -19,7 +19,7 @@ Requires-Python: >=3.12
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: bleak~=1.1.0
22
- Requires-Dist: bleak-retry-connector~=4.0.1
22
+ Requires-Dist: bleak-retry-connector~=4.0.2
23
23
  Requires-Dist: asyncio
24
24
  Requires-Dist: logging
25
25
  Requires-Dist: statistics
@@ -29,6 +29,7 @@ Requires-Dist: pytest; extra == "dev"
29
29
  Requires-Dist: pytest-asyncio; extra == "dev"
30
30
  Requires-Dist: pytest-cov; extra == "dev"
31
31
  Requires-Dist: pytest-xdist; extra == "dev"
32
+ Requires-Dist: hypothesis; extra == "dev"
32
33
  Requires-Dist: mypy; extra == "dev"
33
34
  Requires-Dist: ruff; extra == "dev"
34
35
  Dynamic: license-file
@@ -0,0 +1,36 @@
1
+ aiobmsble/__init__.py,sha256=zIqeiJBneqIUo61GIeqHh0gt9itp19OierNrMLEw25Y,3049
2
+ aiobmsble/__main__.py,sha256=swsFTPO4Cz8fsFwfGfjCv0M6EI9g8VDhC-5HaCo-spI,3113
3
+ aiobmsble/basebms.py,sha256=pPhzj6pJUFtVnbHR5bauRi4BpuWJwQ9GXFueEDCVIZw,18813
4
+ aiobmsble/utils.py,sha256=ckcOXMLTpm4oCxbGKco88cPVP4nOgiTJ16ebFlvsj_E,5805
5
+ aiobmsble/bms/__init__.py,sha256=ZE4Uezyd5fs3os4_bt6Pnzsfrp38LTXItdvJ9-zBiR0,165
6
+ aiobmsble/bms/abc_bms.py,sha256=wud4DTj5Cbo9CjsQ96Tn12CXOSoozvKTIa9pWGpEo1s,5992
7
+ aiobmsble/bms/ant_bms.py,sha256=3YY3Nod6KhylBqYFo2vDgy76MpdYtKbckzDC0SDoEZM,7159
8
+ aiobmsble/bms/braunpwr_bms.py,sha256=_Fl9yQQtzmQyveCQNso6ahX0O1PHxrf5LL4Ef7k5GHg,5976
9
+ aiobmsble/bms/cbtpwr_bms.py,sha256=p4bS3oyVirUFC2-2nbF2EfCrShx8ynpjXEkLPdC0llA,6276
10
+ aiobmsble/bms/cbtpwr_vb_bms.py,sha256=AJhesOKX2yzrsfXQcXufG9E7iX2YJMo-syLKwSMeMLw,6625
11
+ aiobmsble/bms/daly_bms.py,sha256=Ql7Ajv06OSp0m_16vcUy9y_W1JeLOPFWChghD0xr59U,5849
12
+ aiobmsble/bms/dpwrcore_bms.py,sha256=6o4cKtEs8_Fic_se7W32BXlP9K5d3T4_CHiNnnFnMHo,6665
13
+ aiobmsble/bms/dummy_bms.py,sha256=1OvcZByFAPtHhz53JyAaDVZ02a1JkCissTRHQKcYyog,3504
14
+ aiobmsble/bms/ecoworthy_bms.py,sha256=VL3pbU1AtrNBwAIlhjinTiy86Lu8ITvwe55A66GXwGU,5409
15
+ aiobmsble/bms/ective_bms.py,sha256=jvkXaJe0_MvHeV8BiDg7oxCzT_z0WvBvuxybzbbY8Sk,5850
16
+ aiobmsble/bms/ej_bms.py,sha256=B8KWs0Py91TrvvxLKIIwgQ5ppkeQtq8tsQ024jQCeKA,8028
17
+ aiobmsble/bms/felicity_bms.py,sha256=RPTvmnDuedErIiVKdsUR-w8zhF7_IOj_gmk7A2VIHTo,4737
18
+ aiobmsble/bms/jbd_bms.py,sha256=eWXbKIgWEDlj18TWho5Q2K3YfLyhCJVJOgC6tc5VfxU,7166
19
+ aiobmsble/bms/jikong_bms.py,sha256=vl5XQx5-ksYMUaopHdd8Pzw_Rw88zF_0I8m23TPD6BE,11152
20
+ aiobmsble/bms/neey_bms.py,sha256=DsIqt2MP9E6lJACw0wDlkx0gMyIPpCwMvIGLz36Otbc,7665
21
+ aiobmsble/bms/ogt_bms.py,sha256=t1mxa0l2umlH7p3AKSHclieqQGHRYtgt_iFMVQ7ry8U,7987
22
+ aiobmsble/bms/pro_bms.py,sha256=PwP6OeXOj6W3Svu80LOgvqnFaUSdGIm6uTO5If2VhKk,5078
23
+ aiobmsble/bms/redodo_bms.py,sha256=EVFWurOvkCuHplHjn7NTqaY-0TmbRBUtFDjrBXqvUcM,4369
24
+ aiobmsble/bms/renogy_bms.py,sha256=Pju6kZea0G1uGNONcxQymJ_TW6rk02ovGgIn1-dE_68,5079
25
+ aiobmsble/bms/renogy_pro_bms.py,sha256=PO7Q0NaPAf9vzU24PvVubxk7k64L241rQESrk26ul4c,3968
26
+ aiobmsble/bms/roypow_bms.py,sha256=l9oJvTPcvS54DDjgeu8Wn74nvwYaEbaK5F4JHGAt_RE,6168
27
+ aiobmsble/bms/seplos_bms.py,sha256=mgNcYy1E9KMyE-J8jk8mxF2RZ1D3AZtMYNePVq3d0bs,9517
28
+ aiobmsble/bms/seplos_v2_bms.py,sha256=nmzOLHqcaxDnZBCb1i_E1qdF7NN8K10-FAGnnIfjtjA,7493
29
+ aiobmsble/bms/tdt_bms.py,sha256=9mFrjmkNo6YY_7klfvJi1_qq8J53o_Ivep9bPO2El3A,7032
30
+ aiobmsble/bms/tianpwr_bms.py,sha256=U_du6TzYj_SXZ_f_DHVYVCDhku5hwWWdkkITtJJLNX8,4946
31
+ aiobmsble-0.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
+ aiobmsble-0.2.2.dist-info/METADATA,sha256=z4IqdTDc5FUFlbuYNltEDWTkm9zXS4EN59ELJf-NPu8,4711
33
+ aiobmsble-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ aiobmsble-0.2.2.dist-info/entry_points.txt,sha256=HSC_C3nQikc3nk0a6mcG92RuIM7wAzozjBVfDojJceo,54
35
+ aiobmsble-0.2.2.dist-info/top_level.txt,sha256=YHBVzg45mJ3vPz0sl_TpMB0edMqqhD61kwJj4EPAk9g,10
36
+ aiobmsble-0.2.2.dist-info/RECORD,,