Skip to content

Commit 7da0dfe

Browse files
authored
Fix/code improvements (#104)
* clean/unify log messages * rename advertisement data * static methods and class variables * stricter typing
1 parent a44fa31 commit 7da0dfe

14 files changed

+338
-379
lines changed

custom_components/bms_ble/coordinator.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@
66

77
from bleak.backends.device import BLEDevice
88
from bleak.exc import BleakError
9-
10-
from homeassistant.components.bluetooth import (
11-
DOMAIN as BLUETOOTH_DOMAIN,
12-
async_last_service_info,
13-
)
9+
from homeassistant.components.bluetooth import async_last_service_info
10+
from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN
1411
from homeassistant.core import HomeAssistant
1512
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
1613
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -49,7 +46,7 @@ def __init__(
4946
if service_info := async_last_service_info(
5047
self.hass, address=self._mac, connectable=True
5148
):
52-
LOGGER.debug("device data: %s", service_info.as_dict())
49+
LOGGER.debug("%s: advertisement: %s", self.name, service_info.as_dict())
5350

5451
# retrieve BMS class and initialize it
5552
self._device: BaseBMS = bms_device
@@ -84,7 +81,7 @@ def link_quality(self) -> int:
8481

8582
async def async_shutdown(self) -> None:
8683
"""Shutdown coordinator and any connection."""
87-
LOGGER.debug("%s: shuting down BMS device", self.name)
84+
LOGGER.debug("Shutting down BMS device (%s)", self.name)
8885
await super().async_shutdown()
8986
await self._device.disconnect()
9087

@@ -118,5 +115,5 @@ async def _async_update_data(self) -> BMSsample:
118115
)
119116

120117
self._link_q[-1] = True # set success
121-
LOGGER.debug("BMS data sample %s", battery_info)
118+
LOGGER.debug("%s: BMS data sample %s", self.name, battery_info)
122119
return battery_info

custom_components/bms_ble/plugins/basebms.py

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Base class defintion for battery management systems (BMS)."""
22

3-
from abc import ABCMeta, abstractmethod
43
import asyncio.events
5-
from collections.abc import Awaitable, Callable
64
import logging
5+
from abc import ABCMeta, abstractmethod
6+
from collections.abc import Awaitable, Callable
77
from statistics import fmean
88
from typing import Any, Final
99

@@ -12,6 +12,10 @@
1212
from bleak.backends.device import BLEDevice
1313
from bleak.exc import BleakError
1414
from bleak_retry_connector import establish_connection
15+
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
16+
from homeassistant.components.bluetooth.match import ble_device_matches
17+
from homeassistant.loader import BluetoothMatcherOptional
18+
from homeassistant.util.unit_conversion import _HRS_TO_SECS
1519

1620
from custom_components.bms_ble.const import (
1721
ATTR_BATTERY_CHARGING,
@@ -26,12 +30,6 @@
2630
KEY_CELL_VOLTAGE,
2731
KEY_TEMP_VALUE,
2832
)
29-
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
30-
from homeassistant.components.bluetooth.match import (
31-
BluetoothMatcherOptional,
32-
ble_device_matches,
33-
)
34-
from homeassistant.util.unit_conversion import _HRS_TO_SECS
3533

3634
type BMSsample = dict[str, int | float | bool]
3735

@@ -118,8 +116,8 @@ def _calc_values() -> set[str]:
118116
"""
119117
return set()
120118

121-
@classmethod
122-
def _add_missing_values(cls, data: BMSsample, values: set[str]):
119+
@staticmethod
120+
def _add_missing_values(data: BMSsample, values: set[str]):
123121
"""Calculate missing BMS values from existing ones.
124122
125123
data: data dictionary from BMS
@@ -214,7 +212,7 @@ async def _wait_event(self) -> None:
214212

215213
@abstractmethod
216214
async def _async_update(self) -> BMSsample:
217-
"""Return a dictionary of BMS values, where the keys need to match the keys in the SENSOR_TYPES list."""
215+
"""Return a dictionary of BMS values (keys need to come from the SENSOR_TYPES list)."""
218216

219217
async def async_update(self) -> BMSsample:
220218
"""Retrieve updated values from the BMS using method of the subclass."""
@@ -239,3 +237,8 @@ def crc_xmodem(data: bytearray) -> int:
239237
for _ in range(8):
240238
crc = (crc >> 1) ^ 0xA001 if crc % 2 else (crc >> 1)
241239
return ((0xFF00 & crc) >> 8) | ((crc & 0xFF) << 8)
240+
241+
242+
def crc_sum(frame: bytes) -> int:
243+
"""Calculate frame CRC."""
244+
return sum(frame) & 0xFF

custom_components/bms_ble/plugins/cbtpwr_bms.py

+43-49
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Module to support CBT Power Smart BMS."""
22

33
import asyncio
4-
from collections.abc import Callable
54
import logging
5+
from collections.abc import Callable
66
from typing import Any, Final
77

88
from bleak.backends.device import BLEDevice
99
from bleak.uuids import normalize_uuid_str
10+
from homeassistant.util.unit_conversion import _HRS_TO_SECS
1011

1112
from custom_components.bms_ble.const import (
1213
ATTR_BATTERY_CHARGING,
@@ -23,9 +24,8 @@
2324
KEY_CELL_VOLTAGE,
2425
KEY_DESIGN_CAP,
2526
)
26-
from homeassistant.util.unit_conversion import _HRS_TO_SECS
2727

28-
from .basebms import BaseBMS, BMSsample
28+
from .basebms import BaseBMS, BMSsample, crc_sum
2929

3030
BAT_TIMEOUT: Final = 1
3131
LOGGER: Final = logging.getLogger(__name__)
@@ -42,23 +42,23 @@ class BMS(BaseBMS):
4242
LEN_POS: Final[int] = 3
4343
CMD_POS: Final[int] = 2
4444
CELL_VOLTAGE_CMDS: Final[list[int]] = [0x5, 0x6, 0x7, 0x8]
45+
_FIELDS: Final[
46+
list[tuple[str, int, int, int, bool, Callable[[int], int | float]]]
47+
] = [
48+
(ATTR_VOLTAGE, 0x0B, 4, 4, False, lambda x: float(x / 1000)),
49+
(ATTR_CURRENT, 0x0B, 8, 4, True, lambda x: float(x / 1000)),
50+
(ATTR_TEMPERATURE, 0x09, 4, 2, False, lambda x: x),
51+
(ATTR_BATTERY_LEVEL, 0x0A, 4, 1, False, lambda x: x),
52+
(KEY_DESIGN_CAP, 0x15, 4, 2, False, lambda x: x),
53+
(ATTR_CYCLES, 0x15, 6, 2, False, lambda x: x),
54+
(ATTR_RUNTIME, 0x0C, 14, 2, False, lambda x: float(x * _HRS_TO_SECS / 100)),
55+
]
56+
_CMDS: Final[list[int]] = list({field[1] for field in _FIELDS})
4557

4658
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
4759
"""Intialize private BMS members."""
4860
super().__init__(LOGGER, self._notification_handler, ble_device, reconnect)
4961
self._data: bytearray = bytearray()
50-
self._FIELDS: Final[
51-
list[tuple[str, int, int, int, bool, Callable[[int], int | float]]]
52-
] = [
53-
(ATTR_VOLTAGE, 0x0B, 4, 4, False, lambda x: float(x / 1000)),
54-
(ATTR_CURRENT, 0x0B, 8, 4, True, lambda x: float(x / 1000)),
55-
(ATTR_TEMPERATURE, 0x09, 4, 2, False, lambda x: x),
56-
(ATTR_BATTERY_LEVEL, 0x0A, 4, 1, False, lambda x: x),
57-
(KEY_DESIGN_CAP, 0x15, 4, 2, False, lambda x: x),
58-
(ATTR_CYCLES, 0x15, 6, 2, False, lambda x: x),
59-
(ATTR_RUNTIME, 0x0C, 14, 2, False, lambda x: float(x * _HRS_TO_SECS / 100)),
60-
]
61-
self._CMDS: Final[list[int]] = list({field[1] for field in self._FIELDS})
6262

6363
@staticmethod
6464
def matcher_dict_list() -> list[dict[str, Any]]:
@@ -102,54 +102,48 @@ def _calc_values() -> set[str]:
102102

103103
def _notification_handler(self, _sender, data: bytearray) -> None:
104104
"""Retrieve BMS data update."""
105-
106-
LOGGER.debug("(%s) Rx BLE data: %s", self._ble_device.name, data)
105+
LOGGER.debug("%s: Received BLE data: %s", self.name, data)
107106

108107
# verify that data long enough
109-
if (
110-
len(data) < self.MIN_FRAME
111-
or len(data) != self.MIN_FRAME + data[self.LEN_POS]
112-
):
108+
if len(data) < BMS.MIN_FRAME or len(data) != BMS.MIN_FRAME + data[BMS.LEN_POS]:
113109
LOGGER.debug(
114-
"(%s) incorrect frame length (%i): %s", self.name, len(data), data
110+
"%s: incorrect frame length (%i): %s", self.name, len(data), data
115111
)
116112
return
117113

118-
if not data.startswith(self.HEAD) or not data.endswith(self.TAIL_RX):
119-
LOGGER.debug("(%s) Incorrect frame start/end: %s", self.name, data)
114+
if not data.startswith(BMS.HEAD) or not data.endswith(BMS.TAIL_RX):
115+
LOGGER.debug("%s: incorrect frame start/end: %s", self.name, data)
120116
return
121117

122-
crc = self._crc(data[len(self.HEAD) : len(data) + self.CRC_POS])
123-
if data[self.CRC_POS] != crc:
118+
crc = crc_sum(data[len(BMS.HEAD) : len(data) + BMS.CRC_POS])
119+
if data[BMS.CRC_POS] != crc:
124120
LOGGER.debug(
125-
"(%s) Rx data CRC is invalid: 0x%x != 0x%x",
121+
"%s: RX data CRC is invalid: 0x%X != 0x%X",
126122
self.name,
127-
data[len(data) + self.CRC_POS],
123+
data[len(data) + BMS.CRC_POS],
128124
crc,
129125
)
130126
return
131127

132128
self._data = data
133129
self._data_event.set()
134130

135-
def _crc(self, frame: bytes) -> int:
136-
"""Calculate CBT Power frame CRC."""
137-
return sum(frame) & 0xFF
138-
139-
def _gen_frame(self, cmd: bytes, value: list[int] | None = None) -> bytes:
131+
@staticmethod
132+
def _gen_frame(cmd: bytes, value: list[int] | None = None) -> bytes:
140133
"""Assemble a CBT Power BMS command."""
141134
value = [] if value is None else value
142135
assert len(value) <= 255
143-
frame = bytes([*self.HEAD, cmd[0]])
136+
frame = bytes([*BMS.HEAD, cmd[0]])
144137
frame += bytes([len(value), *value])
145-
frame += bytes([self._crc(frame[len(self.HEAD) :])])
146-
frame += bytes([*self.TAIL_TX])
138+
frame += bytes([crc_sum(frame[len(BMS.HEAD) :])])
139+
frame += bytes([*BMS.TAIL_TX])
147140
return frame
148141

149-
def _cell_voltages(self, data: bytearray) -> dict[str, float]:
142+
@staticmethod
143+
def _cell_voltages(data: bytearray) -> dict[str, float]:
150144
"""Return cell voltages from status message."""
151145
return {
152-
f"{KEY_CELL_VOLTAGE}{idx+(data[self.CMD_POS]-5)*5}": int.from_bytes(
146+
f"{KEY_CELL_VOLTAGE}{idx+(data[BMS.CMD_POS]-5)*5}": int.from_bytes(
153147
data[4 + 2 * idx : 6 + 2 * idx],
154148
byteorder="little",
155149
signed=True,
@@ -162,25 +156,25 @@ async def _async_update(self) -> BMSsample:
162156
"""Update battery status information."""
163157
data = {}
164158
resp_cache = {} # variable to avoid multiple queries with same command
165-
for cmd in self._CMDS:
166-
LOGGER.debug("(%s) request command 0x%X.", self.name, cmd)
159+
for cmd in BMS._CMDS:
160+
LOGGER.debug("%s: request command 0x%X.", self.name, cmd)
167161
await self._client.write_gatt_char(
168-
BMS.uuid_tx(), data=self._gen_frame(cmd.to_bytes(1))
162+
BMS.uuid_tx(), data=BMS._gen_frame(cmd.to_bytes(1))
169163
)
170164
try:
171165
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
172166
except TimeoutError:
173167
continue
174-
if cmd != self._data[self.CMD_POS]:
168+
if cmd != self._data[BMS.CMD_POS]:
175169
LOGGER.debug(
176-
"(%s): incorrect response 0x%x to command 0x%x",
170+
"%s:: incorrect response 0x%X to command 0x%X",
177171
self.name,
178-
self._data[self.CMD_POS],
172+
self._data[BMS.CMD_POS],
179173
cmd,
180174
)
181-
resp_cache[self._data[self.CMD_POS]] = self._data.copy()
175+
resp_cache[self._data[BMS.CMD_POS]] = self._data.copy()
182176

183-
for field, cmd, pos, size, sign, fct in self._FIELDS:
177+
for field, cmd, pos, size, sign, fct in BMS._FIELDS:
184178
if resp_cache.get(cmd):
185179
data |= {
186180
field: fct(
@@ -191,15 +185,15 @@ async def _async_update(self) -> BMSsample:
191185
}
192186

193187
voltages = {}
194-
for cmd in self.CELL_VOLTAGE_CMDS:
188+
for cmd in BMS.CELL_VOLTAGE_CMDS:
195189
await self._client.write_gatt_char(
196-
BMS.uuid_tx(), data=self._gen_frame(cmd.to_bytes(1))
190+
BMS.uuid_tx(), data=BMS._gen_frame(cmd.to_bytes(1))
197191
)
198192
try:
199193
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
200194
except TimeoutError:
201195
break
202-
voltages |= self._cell_voltages(self._data)
196+
voltages |= BMS._cell_voltages(self._data)
203197
if invalid := [k for k, v in voltages.items() if v == 0]:
204198
for k in invalid:
205199
voltages.pop(k)

0 commit comments

Comments
 (0)