Add request retries and nominal capacity override number entity

Retries (bluetooth_handler):
- request() now retries up to 3× with 0.5s pause between attempts
- Tries Write With Response first, falls back to Write Without Response
  automatically if the characteristic rejects it — handles both BMS variants

Number entity (number.py):
- "Nominal Capacity (Override)" lets user correct a stale BMS capacity value
  (e.g. after a cell upgrade) without PC software
- Value is restored across HA restarts via RestoreEntity
- Immediately patches coordinator.data so the sensor reflects it without
  waiting for the next poll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 19:57:22 +02:00
parent 5d527168e2
commit 03b63c476a
3 changed files with 158 additions and 16 deletions
@@ -123,22 +123,52 @@ class BmsBluetoothHandler:
# Request / response
# ------------------------------------------------------------------
async def request(self, command: bytes, timeout: float = 5.0) -> bytes | None:
"""Send a command frame and wait for the corresponding response frame."""
async def request(
self,
command: bytes,
timeout: float = 5.0,
retries: int = 3,
) -> bytes | None:
"""Send a command frame and wait for the corresponding response frame.
Retries up to `retries` times with a short pause between attempts to
handle occasional BLE packet loss or a slow BMS response.
Tries Write With Response first; if that raises a GATT error the BMS
likely only supports Write Without Response, so we fall back silently.
"""
async with self._lock:
self._response_event.clear()
self._response_data = None
try:
await self._client.write_gatt_char(TX_CHAR_UUID, command, response=True)
except BleakError as exc:
_LOGGER.error("BLE write failed: %s", exc)
return None
try:
await asyncio.wait_for(self._response_event.wait(), timeout)
return self._response_data
except asyncio.TimeoutError:
_LOGGER.warning("BMS response timeout (cmd=%s)", command.hex())
return None
for attempt in range(1, retries + 1):
self._response_event.clear()
self._response_data = None
try:
await self._client.write_gatt_char(
TX_CHAR_UUID, command, response=True
)
except BleakError:
# Characteristic may not support Write With Response — try without
try:
await self._client.write_gatt_char(
TX_CHAR_UUID, command, response=False
)
except BleakError as exc:
_LOGGER.error("BLE write failed (attempt %d/%d): %s",
attempt, retries, exc)
if attempt < retries:
await asyncio.sleep(0.5)
continue
try:
await asyncio.wait_for(self._response_event.wait(), timeout)
return self._response_data
except asyncio.TimeoutError:
_LOGGER.warning(
"BMS response timeout (cmd=0x%s, attempt %d/%d)",
command.hex(), attempt, retries,
)
if attempt < retries:
await asyncio.sleep(0.5)
return None
# ------------------------------------------------------------------
# Frame parsers