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.
@@ -0,0 +1,205 @@
1
+ """Module to support Seplos v2 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_xmodem
11
+
12
+
13
+ class BMS(BaseBMS):
14
+ """Seplos v2 BMS implementation."""
15
+
16
+ _HEAD: Final[bytes] = b"\x7e"
17
+ _TAIL: Final[bytes] = b"\x0d"
18
+ _CMD_VER: Final[int] = 0x10 # TX protocol version
19
+ _RSP_VER: Final[int] = 0x14 # RX protocol version
20
+ _MIN_LEN: Final[int] = 10
21
+ _MAX_SUBS: Final[int] = 0xF
22
+ _CELL_POS: Final[int] = 9
23
+ _PRB_MAX: Final[int] = 8 # max number of alarm event bytes
24
+ _PRB_MASK: Final[int] = ~0x82FFFF # ignore byte 7-8 + byte 6 (bit 7,2)
25
+ _PFIELDS: Final[tuple[BMSdp, ...]] = ( # Seplos V2: single machine data
26
+ BMSdp("voltage", 2, 2, False, lambda x: x / 100),
27
+ BMSdp("current", 0, 2, True, lambda x: x / 100), # /10 for 0x62
28
+ BMSdp("cycle_charge", 4, 2, False, lambda x: x / 100), # /10 for 0x62
29
+ BMSdp("cycles", 13, 2, False, lambda x: x),
30
+ BMSdp("battery_level", 9, 2, False, lambda x: x / 10),
31
+ )
32
+ _GSMD_LEN: Final[int] = _CELL_POS + max((dp.pos + dp.size) for dp in _PFIELDS) + 3
33
+ _CMDS: Final[list[tuple[int, bytes]]] = [(0x51, b""), (0x61, b"\x00"), (0x62, b"")]
34
+
35
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
36
+ """Initialize BMS."""
37
+ super().__init__(ble_device, reconnect)
38
+ self._data_final: dict[int, bytearray] = {}
39
+ self._exp_len: int = BMS._MIN_LEN
40
+ self._exp_reply: set[int] = set()
41
+
42
+ @staticmethod
43
+ def matcher_dict_list() -> list[MatcherPattern]:
44
+ """Provide BluetoothMatcher definition."""
45
+ return [
46
+ {
47
+ "local_name": pattern,
48
+ "service_uuid": BMS.uuid_services()[0],
49
+ "connectable": True,
50
+ }
51
+ for pattern in ("BP0?", "BP1?", "BP2?")
52
+ ]
53
+
54
+ @staticmethod
55
+ def device_info() -> dict[str, str]:
56
+ """Return device information for the battery management system."""
57
+ return {"manufacturer": "Seplos", "model": "Smart BMS V2"}
58
+
59
+ @staticmethod
60
+ def uuid_services() -> list[str]:
61
+ """Return list of 128-bit UUIDs of services required by BMS."""
62
+ return [normalize_uuid_str("ff00")]
63
+
64
+ @staticmethod
65
+ def uuid_rx() -> str:
66
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
67
+ return "ff01"
68
+
69
+ @staticmethod
70
+ def uuid_tx() -> str:
71
+ """Return 16-bit UUID of characteristic that provides write property."""
72
+ return "ff02"
73
+
74
+ @staticmethod
75
+ def _calc_values() -> frozenset[BMSvalue]:
76
+ return frozenset(
77
+ {
78
+ "battery_charging",
79
+ "cycle_capacity",
80
+ "delta_voltage",
81
+ "power",
82
+ "runtime",
83
+ "temperature",
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
+ if (
92
+ len(data) > BMS._MIN_LEN
93
+ and data.startswith(BMS._HEAD)
94
+ and len(self._data) >= self._exp_len
95
+ ):
96
+ self._exp_len = BMS._MIN_LEN + int.from_bytes(data[5:7])
97
+ self._data = bytearray()
98
+
99
+ self._data += data
100
+ self._log.debug(
101
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
102
+ )
103
+
104
+ # verify that data is long enough
105
+ if len(self._data) < self._exp_len:
106
+ return
107
+
108
+ if not self._data.endswith(BMS._TAIL):
109
+ self._log.debug("incorrect frame end: %s", self._data)
110
+ return
111
+
112
+ if self._data[1] != BMS._RSP_VER:
113
+ self._log.debug("unknown frame version: V%.1f", self._data[1] / 10)
114
+ return
115
+
116
+ if self._data[4]:
117
+ self._log.debug("BMS reported error code: 0x%X", self._data[4])
118
+ return
119
+
120
+ if (crc := crc_xmodem(self._data[1:-3])) != int.from_bytes(self._data[-3:-1]):
121
+ self._log.debug(
122
+ "invalid checksum 0x%X != 0x%X",
123
+ crc,
124
+ int.from_bytes(self._data[-3:-1]),
125
+ )
126
+ return
127
+
128
+ self._log.debug(
129
+ "address: 0x%X, function: 0x%X, return: 0x%X",
130
+ self._data[2],
131
+ self._data[3],
132
+ self._data[4],
133
+ )
134
+
135
+ self._data_final[self._data[3]] = self._data
136
+ try:
137
+ self._exp_reply.remove(self._data[3])
138
+ self._data_event.set()
139
+ except KeyError:
140
+ self._log.debug("unexpected reply: 0x%X", self._data[3])
141
+
142
+ async def _init_connection(
143
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
144
+ ) -> None:
145
+ """Initialize protocol state."""
146
+ await super()._init_connection()
147
+ self._exp_len = BMS._MIN_LEN
148
+
149
+ @staticmethod
150
+ def _cmd(cmd: int, address: int = 0, data: bytearray = bytearray()) -> bytes:
151
+ """Assemble a Seplos V2 BMS command."""
152
+ assert cmd in (0x47, 0x51, 0x61, 0x62, 0x04) # allow only read commands
153
+ frame = bytearray([*BMS._HEAD, BMS._CMD_VER, address, 0x46, cmd])
154
+ frame += len(data).to_bytes(2, "big", signed=False) + data
155
+ frame += int.to_bytes(crc_xmodem(frame[1:]), 2, byteorder="big") + BMS._TAIL
156
+ return bytes(frame)
157
+
158
+ async def _async_update(self) -> BMSsample:
159
+ """Update battery status information."""
160
+
161
+ for cmd, data in BMS._CMDS:
162
+ self._exp_reply.add(cmd)
163
+ await self._await_reply(BMS._cmd(cmd, data=bytearray(data)))
164
+
165
+ result: BMSsample = {}
166
+ result["cell_count"] = self._data_final[0x61][BMS._CELL_POS]
167
+ result["temp_sensors"] = self._data_final[0x61][
168
+ BMS._CELL_POS + result["cell_count"] * 2 + 1
169
+ ]
170
+ ct_blk_len: Final[int] = (result["cell_count"] + result["temp_sensors"]) * 2 + 2
171
+
172
+ if (BMS._GSMD_LEN + ct_blk_len) > len(self._data_final[0x61]):
173
+ raise ValueError("message too short to decode data")
174
+
175
+ result |= BMS._decode_data(
176
+ BMS._PFIELDS, self._data_final[0x61], offset=BMS._CELL_POS + ct_blk_len
177
+ )
178
+
179
+ # get extention pack count from parallel data (main pack)
180
+ result["pack_count"] = self._data_final[0x51][42]
181
+
182
+ # get alarms from parallel data (main pack)
183
+ alarm_evt: Final[int] = min(self._data_final[0x62][46], BMS._PRB_MAX)
184
+ result["problem_code"] = (
185
+ int.from_bytes(self._data_final[0x62][47 : 47 + alarm_evt], byteorder="big")
186
+ & BMS._PRB_MASK
187
+ )
188
+
189
+ result["cell_voltages"] = BMS._cell_voltages(
190
+ self._data_final[0x61],
191
+ cells=self._data_final[0x61][BMS._CELL_POS],
192
+ start=10,
193
+ )
194
+ result["temp_values"] = BMS._temp_values(
195
+ self._data_final[0x61],
196
+ values=result["temp_sensors"],
197
+ start=BMS._CELL_POS + result.get("cell_count", 0) * 2 + 2,
198
+ signed=False,
199
+ offset=2731,
200
+ divider=10,
201
+ )
202
+
203
+ self._data_final.clear()
204
+
205
+ return result
@@ -0,0 +1,199 @@
1
+ """Module to support TDT 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
+ """TDT BMS implementation."""
15
+
16
+ _UUID_CFG: Final[str] = "fffa"
17
+ _HEAD: Final[int] = 0x7E
18
+ _CMD_HEADS: list[int] = [0x7E, 0x1E] # alternative command head
19
+ _TAIL: Final[int] = 0x0D
20
+ _CMD_VER: Final[int] = 0x00
21
+ _RSP_VER: Final[frozenset[int]] = frozenset({0x00, 0x04})
22
+ _CELL_POS: Final[int] = 0x8
23
+ _INFO_LEN: Final[int] = 10 # minimal frame length
24
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
25
+ BMSdp("voltage", 2, 2, False, lambda x: x / 100, 0x8C),
26
+ BMSdp(
27
+ "current",
28
+ 0,
29
+ 2,
30
+ False,
31
+ lambda x: (x & 0x3FFF) / 10 * (-1 if x >> 15 else 1),
32
+ 0x8C,
33
+ ),
34
+ BMSdp("cycle_charge", 4, 2, False, lambda x: x / 10, 0x8C),
35
+ BMSdp("battery_level", 13, 1, False, lambda x: x, 0x8C),
36
+ BMSdp("cycles", 8, 2, False, lambda x: x, 0x8C),
37
+ ) # problem code is not included in the list, but extra
38
+ _CMDS: Final[list[int]] = [*list({field.idx for field in _FIELDS}), 0x8D]
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
+ self._cmd_heads: list[int] = BMS._CMD_HEADS
45
+ self._exp_len: int = 0
46
+
47
+ @staticmethod
48
+ def matcher_dict_list() -> list[MatcherPattern]:
49
+ """Provide BluetoothMatcher definition."""
50
+ return [{"manufacturer_id": 54976, "connectable": True}]
51
+
52
+ @staticmethod
53
+ def device_info() -> dict[str, str]:
54
+ """Return device information for the battery management system."""
55
+ return {"manufacturer": "TDT", "model": "Smart BMS"}
56
+
57
+ @staticmethod
58
+ def uuid_services() -> list[str]:
59
+ """Return list of 128-bit UUIDs of services required by BMS."""
60
+ return [normalize_uuid_str("fff0")]
61
+
62
+ @staticmethod
63
+ def uuid_rx() -> str:
64
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
65
+ return "fff1"
66
+
67
+ @staticmethod
68
+ def uuid_tx() -> str:
69
+ """Return 16-bit UUID of characteristic that provides write property."""
70
+ return "fff2"
71
+
72
+ @staticmethod
73
+ def _calc_values() -> frozenset[BMSvalue]:
74
+ return frozenset(
75
+ {
76
+ "battery_charging",
77
+ "cycle_capacity",
78
+ "delta_voltage",
79
+ "power",
80
+ "runtime",
81
+ "temperature",
82
+ }
83
+ ) # calculate further values from BMS provided set ones
84
+
85
+ async def _init_connection(
86
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
87
+ ) -> None:
88
+ await self._await_reply(
89
+ data=b"HiLink", char=BMS._UUID_CFG, wait_for_notify=False
90
+ )
91
+ if (
92
+ ret := int.from_bytes(await self._client.read_gatt_char(BMS._UUID_CFG))
93
+ ) != 0x1:
94
+ self._log.debug("error unlocking BMS: %X", ret)
95
+
96
+ await super()._init_connection()
97
+
98
+ def _notification_handler(
99
+ self, _sender: BleakGATTCharacteristic, data: bytearray
100
+ ) -> None:
101
+ """Handle the RX characteristics notify event (new data arrives)."""
102
+ self._log.debug("RX BLE data: %s", data)
103
+
104
+ if (
105
+ len(data) > BMS._INFO_LEN
106
+ and data[0] == BMS._HEAD
107
+ and len(self._data) >= self._exp_len
108
+ ):
109
+ self._exp_len = BMS._INFO_LEN + int.from_bytes(data[6:8])
110
+ self._data = bytearray()
111
+
112
+ self._data += data
113
+ self._log.debug(
114
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
115
+ )
116
+
117
+ # verify that data is long enough
118
+ if len(self._data) < max(BMS._INFO_LEN, self._exp_len):
119
+ return
120
+
121
+ if self._data[-1] != BMS._TAIL:
122
+ self._log.debug("frame end incorrect: %s", self._data)
123
+ return
124
+
125
+ if self._data[1] not in BMS._RSP_VER:
126
+ self._log.debug("unknown frame version: V%.1f", self._data[1] / 10)
127
+ return
128
+
129
+ if self._data[4]:
130
+ self._log.debug("BMS reported error code: 0x%X", self._data[4])
131
+ return
132
+
133
+ if (crc := crc_modbus(self._data[:-3])) != int.from_bytes(
134
+ self._data[-3:-1], "big"
135
+ ):
136
+ self._log.debug(
137
+ "invalid checksum 0x%X != 0x%X",
138
+ int.from_bytes(self._data[-3:-1], "big"),
139
+ crc,
140
+ )
141
+ return
142
+ self._data_final[self._data[5]] = self._data
143
+ self._data_event.set()
144
+
145
+ @staticmethod
146
+ def _cmd(cmd: int, data: bytearray = bytearray(), cmd_head: int = _HEAD) -> bytes:
147
+ """Assemble a TDT BMS command."""
148
+ assert cmd in (0x8C, 0x8D, 0x92) # allow only read commands
149
+
150
+ frame = bytearray([cmd_head, BMS._CMD_VER, 0x1, 0x3, 0x0, cmd])
151
+ frame += len(data).to_bytes(2, "big", signed=False) + data
152
+ frame += crc_modbus(frame).to_bytes(2, "big") + bytes([BMS._TAIL])
153
+
154
+ return bytes(frame)
155
+
156
+ async def _async_update(self) -> BMSsample:
157
+ """Update battery status information."""
158
+
159
+ for head in self._cmd_heads:
160
+ try:
161
+ for cmd in BMS._CMDS:
162
+ await self._await_reply(BMS._cmd(cmd, cmd_head=head))
163
+ self._cmd_heads = [head] # set to single head for further commands
164
+ break
165
+ except TimeoutError:
166
+ ... # try next command head
167
+ else:
168
+ raise TimeoutError
169
+
170
+ result: BMSsample = {"cell_count": self._data_final[0x8C][BMS._CELL_POS]}
171
+ result["temp_sensors"] = self._data_final[0x8C][
172
+ BMS._CELL_POS + result["cell_count"] * 2 + 1
173
+ ]
174
+
175
+ result["cell_voltages"] = BMS._cell_voltages(
176
+ self._data_final[0x8C],
177
+ cells=result.get("cell_count", 0),
178
+ start=BMS._CELL_POS + 1,
179
+ )
180
+ result["temp_values"] = BMS._temp_values(
181
+ self._data_final[0x8C],
182
+ values=result["temp_sensors"],
183
+ start=BMS._CELL_POS + result.get("cell_count", 0) * 2 + 2,
184
+ signed=False,
185
+ offset=2731,
186
+ divider=10,
187
+ )
188
+ idx: Final[int] = result.get("cell_count", 0) + result.get("temp_sensors", 0)
189
+
190
+ result |= BMS._decode_data(
191
+ BMS._FIELDS, self._data_final, offset=BMS._CELL_POS + idx * 2 + 2
192
+ )
193
+ result["problem_code"] = int.from_bytes(
194
+ self._data_final[0x8D][BMS._CELL_POS + idx + 6 : BMS._CELL_POS + idx + 8]
195
+ )
196
+
197
+ self._data_final.clear()
198
+
199
+ return result
@@ -0,0 +1,138 @@
1
+ """Module to support TianPwr 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
11
+
12
+
13
+ class BMS(BaseBMS):
14
+ """TianPwr BMS implementation."""
15
+
16
+ _HEAD: Final[bytes] = b"\x55"
17
+ _TAIL: Final[bytes] = b"\xaa"
18
+ _RDCMD: Final[bytes] = b"\x04"
19
+ _MAX_CELLS: Final[int] = 16
20
+ _MAX_TEMP: Final[int] = 6
21
+ _MIN_LEN: Final[int] = 4
22
+ _DEF_LEN: Final[int] = 20
23
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
24
+ BMSdp("battery_level", 3, 2, False, lambda x: x, 0x83),
25
+ BMSdp("voltage", 5, 2, False, lambda x: x / 100, 0x83),
26
+ BMSdp("current", 13, 2, True, lambda x: x / 100, 0x83),
27
+ BMSdp("problem_code", 11, 8, False, lambda x: x, 0x84),
28
+ BMSdp("cell_count", 3, 1, False, lambda x: x, 0x84),
29
+ BMSdp("temp_sensors", 4, 1, False, lambda x: x, 0x84),
30
+ BMSdp("design_capacity", 5, 2, False, lambda x: x // 100, 0x84),
31
+ BMSdp("cycle_charge", 7, 2, False, lambda x: x / 100, 0x84),
32
+ BMSdp("cycles", 9, 2, False, lambda x: x, 0x84),
33
+ )
34
+ _CMDS: Final[set[int]] = set({field.idx for field in _FIELDS}) | set({0x87})
35
+
36
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
37
+ """Initialize BMS."""
38
+ super().__init__(ble_device, reconnect)
39
+ self._data_final: dict[int, bytearray] = {}
40
+
41
+ @staticmethod
42
+ def matcher_dict_list() -> list[MatcherPattern]:
43
+ """Provide BluetoothMatcher definition."""
44
+ return [{"local_name": "TP_*", "connectable": True}]
45
+
46
+ @staticmethod
47
+ def device_info() -> dict[str, str]:
48
+ """Return device information for the battery management system."""
49
+ return {"manufacturer": "TianPwr", "model": "SmartBMS"}
50
+
51
+ @staticmethod
52
+ def uuid_services() -> list[str]:
53
+ """Return list of 128-bit UUIDs of services required by BMS."""
54
+ return [normalize_uuid_str("ff00")]
55
+
56
+ @staticmethod
57
+ def uuid_rx() -> str:
58
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
59
+ return "ff01"
60
+
61
+ @staticmethod
62
+ def uuid_tx() -> str:
63
+ """Return 16-bit UUID of characteristic that provides write property."""
64
+ return "ff02"
65
+
66
+ @staticmethod
67
+ def _calc_values() -> frozenset[BMSvalue]:
68
+ return frozenset(
69
+ {
70
+ "battery_charging",
71
+ "cycle_capacity",
72
+ "delta_voltage",
73
+ "power",
74
+ "temperature",
75
+ }
76
+ ) # calculate further values from BMS provided set ones
77
+
78
+ def _notification_handler(
79
+ self, _sender: BleakGATTCharacteristic, data: bytearray
80
+ ) -> None:
81
+ """Handle the RX characteristics notify event (new data arrives)."""
82
+ self._log.debug("RX BLE data: %s", data)
83
+
84
+ # verify that data is long enough
85
+ if len(data) != BMS._DEF_LEN:
86
+ self._log.debug("incorrect frame length")
87
+ return
88
+
89
+ if not data.startswith(BMS._HEAD):
90
+ self._log.debug("incorrect SOF.")
91
+ return
92
+
93
+ if not data.endswith(BMS._TAIL):
94
+ self._log.debug("incorrect EOF.")
95
+ return
96
+
97
+ self._data_final[data[2]] = data.copy()
98
+ self._data_event.set()
99
+
100
+ @staticmethod
101
+ def _cmd(addr: int) -> bytes:
102
+ """Assemble a TianPwr BMS command."""
103
+ return BMS._HEAD + BMS._RDCMD + addr.to_bytes(1) + BMS._TAIL
104
+
105
+ async def _async_update(self) -> BMSsample:
106
+ """Update battery status information."""
107
+
108
+ self._data_final.clear()
109
+ for cmd in BMS._CMDS:
110
+ await self._await_reply(BMS._cmd(cmd))
111
+
112
+ result: BMSsample = BMS._decode_data(BMS._FIELDS, self._data_final)
113
+
114
+ for cmd in range(
115
+ 0x88, 0x89 + min(result.get("cell_count", 0), BMS._MAX_CELLS) // 8
116
+ ):
117
+ await self._await_reply(BMS._cmd(cmd))
118
+ result["cell_voltages"] = result.setdefault(
119
+ "cell_voltages", []
120
+ ) + BMS._cell_voltages(
121
+ self._data_final.get(cmd, bytearray()), cells=8, start=3
122
+ )
123
+
124
+ if {0x83, 0x87}.issubset(self._data_final):
125
+ result["temp_values"] = [
126
+ int.from_bytes(
127
+ self._data_final[0x83][idx : idx + 2], byteorder="big", signed=True
128
+ )
129
+ / 10
130
+ for idx in (7, 11) # take ambient and mosfet temperature
131
+ ] + BMS._temp_values(
132
+ self._data_final.get(0x87, bytearray()),
133
+ values=min(BMS._MAX_TEMP, result.get("temp_sensors", 0)),
134
+ start=3,
135
+ divider=10,
136
+ )
137
+
138
+ return result
aiobmsble/utils.py CHANGED
@@ -20,7 +20,7 @@ def _advertisement_matches(
20
20
  """Determine whether the given advertisement data matches the specified pattern.
21
21
 
22
22
  Args:
23
- matcher (AdvertisementPattern): A dictionary containing the matching criteria.
23
+ matcher (MatcherPattern): A dictionary containing the matching criteria.
24
24
  Possible keys include:
25
25
  - "service_uuid" (str): A specific service 128-bit UUID to match.
26
26
  - "service_data_uuid" (str): A specific service data UUID to match.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiobmsble
3
- Version: 0.2.0
3
+ Version: 0.2.1
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=gwWO0ojesAR4aYsu5P4LZYlaQgkJpMkzg4Q27V3VOrg,2932
2
+ aiobmsble/__main__.py,sha256=7h8ZnUlQ67IMjnRMJJt091ndGIrYuvd5i12OEMxy2j0,3000
3
+ aiobmsble/basebms.py,sha256=bNik9TsHTlAqAR-RPxFFXy6qD1E7l7f53u8wffMgQyU,18556
4
+ aiobmsble/utils.py,sha256=sXAiXncVWLdO-7q9tbwaEbCKf4ciNrRQZEExbxFqwkk,5518
5
+ aiobmsble/bms/__init__.py,sha256=YAtb8MKhgbZP528ISmxwfXtDix7G_3xuREdcDPvDvqo,60
6
+ aiobmsble/bms/abc_bms.py,sha256=1EISRUQ_owkgMAjuX8nGZav7cGdE3IFSpQXVndN8ZdA,5887
7
+ aiobmsble/bms/ant_bms.py,sha256=vb7886qYPU1Rpr-lkGfcMwybySz1bLgAnDGrfnU498U,7054
8
+ aiobmsble/bms/braunpwr_bms.py,sha256=yI82UqJHgS_yKycbTLyBArQo7e8HjFwCnMZG-OlUcHE,5871
9
+ aiobmsble/bms/cbtpwr_bms.py,sha256=d5rTZwZNkhB4Lx2qlilPFPLITweBOGwCyzfwKeXBw-I,6171
10
+ aiobmsble/bms/cbtpwr_vb_bms.py,sha256=2NpwkyUueORVijhywYG_RTRbX_EbLHChS8vPCTLzPo0,6520
11
+ aiobmsble/bms/daly_bms.py,sha256=ebOTBiC9PQNEs7RRw8pyNnWW4zE0rgGK_WD0IOVjvt8,5744
12
+ aiobmsble/bms/dpwrcore_bms.py,sha256=Mt5ZyucPCvn-lWAilKsDxjvrkT123t_cSQ9M8jMEFyo,6560
13
+ aiobmsble/bms/dummy_bms.py,sha256=M6H0o2h2g3LHW-e2JmMMH2RwvviLWu2OuVTAjCd1Nxc,3399
14
+ aiobmsble/bms/ecoworthy_bms.py,sha256=u0ysJmRAVEkNwVFG4jzl2yoA4JuRjGgDN-G84rVrjw4,5304
15
+ aiobmsble/bms/ective_bms.py,sha256=wDwV8EBRFD8s4819d0fxBZ0jUgT1T7Edgn7TmhDCk4M,5745
16
+ aiobmsble/bms/ej_bms.py,sha256=Nu8R-SaqYhM57y2qCJ-ryQfJ9OZTCj7NriiJXBuJWDc,7923
17
+ aiobmsble/bms/felicity_bms.py,sha256=RNNTxJmpj-oqi_-ec2BV82I5Yw5olvnO5KMO6ssrOXU,4632
18
+ aiobmsble/bms/jbd_bms.py,sha256=3W2AFwnRZQ1IT5BFeWOJDQ3ZsXaxquABw34g3pBTGAc,7061
19
+ aiobmsble/bms/jikong_bms.py,sha256=4-xqd3AxmsDYwHDNtKADi-d2lU9ZiHtcvWpSBE1rlOE,11047
20
+ aiobmsble/bms/neey_bms.py,sha256=NU8qDaPO90hQD3kC9w-xoc1v0eLzLM_g-OfeI4bzwxw,7560
21
+ aiobmsble/bms/ogt_bms.py,sha256=k-3FlBMeEZInUqYrAFfI38t9VuKsn7k64etlvyjtIoo,7882
22
+ aiobmsble/bms/pro_bms.py,sha256=64tnmZYxz6fK1fUg6A9x3yNjeYTwEpOQL6Zi-w4bbqU,4973
23
+ aiobmsble/bms/redodo_bms.py,sha256=qxPDCPNiDl_Iq1MDMiG3ZzoAxRHqPXkydzt4DpPLvAs,4264
24
+ aiobmsble/bms/renogy_bms.py,sha256=ua_1mEJANyy4qJIo53Upaa47_soL4WrmeRHErG5eaZU,5027
25
+ aiobmsble/bms/renogy_pro_bms.py,sha256=jTxmeAkSjbJjuhtwB1AFZp_dxDCs8ofCJZlBMwMcLUw,3863
26
+ aiobmsble/bms/roypow_bms.py,sha256=3JGIUY91wzBroS_f0NhBF3jj9XJTniENFcSOaGmMipQ,6063
27
+ aiobmsble/bms/seplos_bms.py,sha256=BLKna81ocFGoJ2E2nfMU7NGvJXSICapggk_fjS_vKBw,9412
28
+ aiobmsble/bms/seplos_v2_bms.py,sha256=3bkZce8wxxG-0InOIPPa-1y6fR2GuZomNQI65ypyJ_s,7388
29
+ aiobmsble/bms/tdt_bms.py,sha256=4P52WRikQLqu6mN40I_upVpmEDKA5gS472EbjhJ7n-Q,6927
30
+ aiobmsble/bms/tianpwr_bms.py,sha256=SKS31KOHVXnvXbPtSAWPUdiI4j8bV7Y9bwRkrpKWZDw,4841
31
+ aiobmsble-0.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
+ aiobmsble-0.2.1.dist-info/METADATA,sha256=Q3p5fiUxVeWt7z2XjztBI--mTbkhCa7ip3W7c-dv2pI,4711
33
+ aiobmsble-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
+ aiobmsble-0.2.1.dist-info/entry_points.txt,sha256=HSC_C3nQikc3nk0a6mcG92RuIM7wAzozjBVfDojJceo,54
35
+ aiobmsble-0.2.1.dist-info/top_level.txt,sha256=YHBVzg45mJ3vPz0sl_TpMB0edMqqhD61kwJj4EPAk9g,10
36
+ aiobmsble-0.2.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- aiobmsble/__init__.py,sha256=gwWO0ojesAR4aYsu5P4LZYlaQgkJpMkzg4Q27V3VOrg,2932
2
- aiobmsble/__main__.py,sha256=7h8ZnUlQ67IMjnRMJJt091ndGIrYuvd5i12OEMxy2j0,3000
3
- aiobmsble/basebms.py,sha256=bNik9TsHTlAqAR-RPxFFXy6qD1E7l7f53u8wffMgQyU,18556
4
- aiobmsble/utils.py,sha256=9DQT4y7VGlSgCEjfTyu3fqKxOBu97cT9V8y8Ce1MTgA,5524
5
- aiobmsble-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
6
- aiobmsble-0.2.0.dist-info/METADATA,sha256=6_PNHNaXNX5GuU_HXB-PWZdrJB5fgKzMoWbYBt0Cj1w,4669
7
- aiobmsble-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- aiobmsble-0.2.0.dist-info/entry_points.txt,sha256=HSC_C3nQikc3nk0a6mcG92RuIM7wAzozjBVfDojJceo,54
9
- aiobmsble-0.2.0.dist-info/top_level.txt,sha256=YHBVzg45mJ3vPz0sl_TpMB0edMqqhD61kwJj4EPAk9g,10
10
- aiobmsble-0.2.0.dist-info/RECORD,,