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,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