Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new sensor for BMS alarms #159

Merged
merged 33 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5e5b8d9
add state sensor
patman15 Jan 10, 2025
becbf90
errors for daly_bms
patman15 Jan 10, 2025
89d5f31
Merge branch 'main' into feature/alarms
patman15 Jan 11, 2025
6375ade
rename problem attribute
patman15 Jan 11, 2025
b8c12c5
check new binary sensor
patman15 Jan 11, 2025
2a7e2e0
skip data calculation if set is fully empty
patman15 Jan 16, 2025
13c0937
fix test race condition for LQ sensor
patman15 Jan 16, 2025
9805a6b
Update test_daly_bms.py
patman15 Jan 17, 2025
56f322e
fix OGT test
patman15 Jan 18, 2025
3900d06
rename test case for problems
patman15 Jan 18, 2025
aa59eff
fixed typo in lambda funciton
patman15 Jan 18, 2025
115fba6
add problem detection for JBD
patman15 Jan 18, 2025
d8ac837
update version
patman15 Jan 19, 2025
5d1bd69
problem detection for OGT
patman15 Jan 26, 2025
b960e83
added problem code
patman15 Jan 26, 2025
204c3e3
test basic problem reports
patman15 Jan 26, 2025
8237a9a
add problem detection for CBTpwr
patman15 Jan 26, 2025
94b194c
Update README.md
patman15 Jan 26, 2025
3b8bed1
add problem detection for Ective BMS
patman15 Jan 29, 2025
d7c5de4
add problem detection for EJ BMS
patman15 Jan 29, 2025
e3fd621
add problem detection for TDT BMS
patman15 Feb 8, 2025
b46c069
add problem detection for D-powercore BMS
patman15 Feb 9, 2025
48c2669
Merge branch 'main' into feature/problems
patman15 Feb 9, 2025
a421521
add problem detection for Seplos v3
patman15 Feb 10, 2025
6c7f62e
add problem detection for JK BMS
patman15 Feb 11, 2025
f6fdcba
add problem detection for Redodo BMS
patman15 Feb 11, 2025
0d1f650
added problem detection for Seplos v2
patman15 Feb 12, 2025
4a629c8
fix josepy incompatibility
patman15 Feb 12, 2025
e460911
code cleanup
patman15 Feb 12, 2025
76065a1
Merge branch 'main' into feature/problems
patman15 Feb 15, 2025
a4acaa1
Merge branch 'main' into feature/alarms
patman15 Feb 19, 2025
d6583c8
fix ruff check
patman15 Feb 19, 2025
f6b0ae9
finalize problem detection for felicity
patman15 Feb 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ This integration allows to monitor Bluetooth Low Energy (BLE) battery management
Platform | Description | Unit | Decription | optional Attributes
-- | -- | -- | -- | --
`binary_sensor` | battery charging | `bool` | indicates `True` if battery is charging
`binary_sensor` | problem | `bool` | indicates `True` if the battery is reports an issue or plausibility checks on values fail | problem code
`sensor` | charge cycles | `#` | lifetime number of charge cycles | package charge cycles
`sensor` | current | `A` | positive for charging, negative for discharging | balance current, package current
`sensor` | delta voltage | `V` | maximum difference between any two cells | cell voltages
Expand Down Expand Up @@ -178,7 +179,6 @@ Once pairing is done, the integration should automatically detect the BMS.

## Outlook
- Clean-up of translations
- Implement status report of the BMS, e.g. warnings, errors as (single) binary sensor (ok, not ok)
- Add option to only have temporary connections (lowers reliability, but helps running more devices via [ESPHome Bluetooth proxy][btproxy-url])
- Add further battery types on [request](https://github.com/patman15/BMS_BLE-HA/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml)

Expand All @@ -193,10 +193,11 @@ for helping with making the integration better.
- Jikong BMS: [esphome-jk-bms](https://github.com/syssi/esphome-jk-bms)
- JBD BMS: [esphome-jbd-bms](https://github.com/syssi/esphome-jbd-bms)
- D-powercore BMS: [Strom BMS monitor](https://github.com/majonessyltetoy/strom)
- Redodo BMS: [LiTime BMS bluetooth](https://github.com/calledit/LiTime_BMS_bluetooth)

[license-shield]: https://img.shields.io/github/license/patman15/BMS_BLE-HA.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/patman15/BMS_BLE-HA.svg?style=for-the-badge
[releases]: https://github.com//patman15/BMS_BLE-HA/releases
[effort-shield]: https://img.shields.io/badge/Effort%20spent-338_hours-gold?style=for-the-badge&cacheSeconds=86400
[effort-shield]: https://img.shields.io/badge/Effort%20spent-369_hours-gold?style=for-the-badge&cacheSeconds=86400
[install-shield]: https://img.shields.io/badge/dynamic/json?style=for-the-badge&color=green&label=Analytics&suffix=%20Installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.bms_ble.total
[btproxy-url]: https://esphome.io/components/bluetooth_proxy
20 changes: 17 additions & 3 deletions custom_components/bms_ble/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import ATTR_BATTERY_CHARGING
from homeassistant.const import ATTR_BATTERY_CHARGING, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from . import BTBmsConfigEntry
from .const import DOMAIN
from .const import ATTR_PROBLEM, DOMAIN, KEY_PROBLEM
from .coordinator import BTBmsCoordinator

BINARY_SENSOR_TYPES: list[BinarySensorEntityDescription] = [
BinarySensorEntityDescription(
key=ATTR_BATTERY_CHARGING,
translation_key=ATTR_BATTERY_CHARGING,
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
)
),
BinarySensorEntityDescription(
key=ATTR_PROBLEM,
translation_key=ATTR_PROBLEM,
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
]


Expand Down Expand Up @@ -58,3 +64,11 @@ def __init__(
def is_on(self) -> bool | None: # type: ignore[reportIncompatibleVariableOverride]
"""Handle updated data from the coordinator."""
return bool(self.coordinator.data.get(self.entity_description.key))

@property
def extra_state_attributes(self) -> dict | None: # type: ignore[reportIncompatibleVariableOverride]
"""Return entity specific state attributes, e.g. problem code."""
# add cell voltages to delta voltage sensor
if self.entity_description.key == ATTR_PROBLEM:
return {KEY_PROBLEM: self.coordinator.data.get(self.entity_description.key)}
return None
2 changes: 2 additions & 0 deletions custom_components/bms_ble/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
ATTR_DELTA_VOLTAGE: Final[str] = "delta_voltage" # [V]
ATTR_LQ: Final[str] = "link_quality" # [%]
ATTR_POWER: Final[str] = "power" # [W]
ATTR_PROBLEM: Final[str] = "problem" # [bool]
ATTR_RSSI: Final[str] = "rssi" # [dBm]
ATTR_RUNTIME: Final[str] = "runtime" # [s]
ATTR_TEMP_SENSORS: Final[str] = "temperature_sensors" # [°C]
Expand All @@ -49,5 +50,6 @@
KEY_DESIGN_CAP: Final[str] = "design_capacity" # [Ah]
KEY_PACK: Final[str] = "pack" # prefix for pack sensors
KEY_PACK_COUNT: Final[str] = "pack_count" # [#]
KEY_PROBLEM: Final[str] = "problem_code" # [#]
KEY_TEMP_SENS: Final[str] = "temp_sensors" # [#]
KEY_TEMP_VALUE: Final[str] = "temp#" # [°C]
2 changes: 1 addition & 1 deletion custom_components/bms_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,5 @@
"bleak_retry_connector"
],
"requirements": [],
"version": "1.12.0"
"version": "1.13.0"
}
31 changes: 27 additions & 4 deletions custom_components/bms_ble/plugins/basebms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@

from custom_components.bms_ble.const import (
ATTR_BATTERY_CHARGING,
ATTR_BATTERY_LEVEL,
ATTR_CURRENT,
ATTR_CYCLE_CAP,
ATTR_CYCLE_CHRG,
ATTR_DELTA_VOLTAGE,
ATTR_POWER,
ATTR_PROBLEM,
ATTR_RUNTIME,
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
KEY_TEMP_VALUE,
)
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
Expand All @@ -36,7 +39,8 @@
class BaseBMS(metaclass=ABCMeta):
"""Base class for battery management system."""

BAT_TIMEOUT: float = 10
BAT_TIMEOUT = 10
MAX_CELL_VOLTAGE: Final[float] = 5.906 # max cell potential

def __init__(
self,
Expand Down Expand Up @@ -139,7 +143,7 @@ def _add_missing_values(data: BMSsample, values: set[str]) -> None:
data: data dictionary from BMS
values: list of values to add to the dictionary
"""
if not values:
if not values or not data:
return

def can_calc(value: str, using: frozenset[str]) -> bool:
Expand Down Expand Up @@ -183,8 +187,27 @@ def can_calc(value: str, using: frozenset[str]) -> bool:
)
# calculate temperature (average of all sensors)
if can_calc(ATTR_TEMPERATURE, frozenset({f"{KEY_TEMP_VALUE}0"})):
temp_values = [v for k, v in data.items() if k.startswith(KEY_TEMP_VALUE)]
data[ATTR_TEMPERATURE] = round(fmean(temp_values), 3)
data[ATTR_TEMPERATURE] = round(
fmean([v for k, v in data.items() if k.startswith(KEY_TEMP_VALUE)]),
3,
)

# do sanity check on values to set problem state
data[ATTR_PROBLEM] = (
data.get(ATTR_PROBLEM, False)
or bool(data.get(KEY_PROBLEM, False))
or (
data.get(ATTR_VOLTAGE, 1) <= 0
or any(
v <= 0 or v > BaseBMS.MAX_CELL_VOLTAGE
for k, v in data.items()
if k.startswith(KEY_CELL_VOLTAGE)
)
or data.get(ATTR_DELTA_VOLTAGE, 0) > BaseBMS.MAX_CELL_VOLTAGE
or data.get(ATTR_CYCLE_CHRG, 1) <= 0
or data.get(ATTR_BATTERY_LEVEL, 0) > 100
)
)

def _on_disconnect(self, _client: BleakClient) -> None:
"""Disconnect callback function."""
Expand Down
2 changes: 2 additions & 0 deletions custom_components/bms_ble/plugins/cbtpwr_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
KEY_DESIGN_CAP,
KEY_PROBLEM,
)
from homeassistant.util.unit_conversion import _HRS_TO_SECS

Expand Down Expand Up @@ -49,6 +50,7 @@ class BMS(BaseBMS):
(KEY_DESIGN_CAP, 0x15, 4, 2, False, lambda x: x),
(ATTR_CYCLES, 0x15, 6, 2, False, lambda x: x),
(ATTR_RUNTIME, 0x0C, 14, 2, False, lambda x: float(x * _HRS_TO_SECS / 100)),
(KEY_PROBLEM, 0x21, 4, 4, False, lambda x: x),
]
_CMDS: Final[list[int]] = list({field[1] for field in _FIELDS})

Expand Down
26 changes: 15 additions & 11 deletions custom_components/bms_ble/plugins/daly_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ATTR_VOLTAGE,
KEY_CELL_COUNT,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
KEY_TEMP_SENS,
KEY_TEMP_VALUE,
)
Expand All @@ -40,15 +41,16 @@ class BMS(BaseBMS):
MAX_TEMP: Final[int] = 8
INFO_LEN: Final[int] = 84 + HEAD_LEN + CRC_LEN + MAX_CELLS + MAX_TEMP
MOS_TEMP_POS: Final[int] = HEAD_LEN + 8
_FIELDS: Final[list[tuple[str, int, Callable[[int], int | float]]]] = [
(ATTR_VOLTAGE, 80 + HEAD_LEN, lambda x: float(x / 10)),
(ATTR_CURRENT, 82 + HEAD_LEN, lambda x: float((x - 30000) / 10)),
(ATTR_BATTERY_LEVEL, 84 + HEAD_LEN, lambda x: float(x / 10)),
(ATTR_CYCLE_CHRG, 96 + HEAD_LEN, lambda x: float(x / 10)),
(KEY_CELL_COUNT, 98 + HEAD_LEN, lambda x: min(x, BMS.MAX_CELLS)),
(KEY_TEMP_SENS, 100 + HEAD_LEN, lambda x: min(x, BMS.MAX_TEMP)),
(ATTR_CYCLES, 102 + HEAD_LEN, lambda x: x),
(ATTR_DELTA_VOLTAGE, 112 + HEAD_LEN, lambda x: float(x / 1000)),
_FIELDS: Final[list[tuple[str, int, int, Callable[[int], int | float]]]] = [
(ATTR_VOLTAGE, 80 + HEAD_LEN, 2, lambda x: float(x / 10)),
(ATTR_CURRENT, 82 + HEAD_LEN, 2, lambda x: float((x - 30000) / 10)),
(ATTR_BATTERY_LEVEL, 84 + HEAD_LEN, 2, lambda x: float(x / 10)),
(ATTR_CYCLE_CHRG, 96 + HEAD_LEN, 2, lambda x: float(x / 10)),
(KEY_CELL_COUNT, 98 + HEAD_LEN, 2, lambda x: min(x, BMS.MAX_CELLS)),
(KEY_TEMP_SENS, 100 + HEAD_LEN, 2, lambda x: min(x, BMS.MAX_TEMP)),
(ATTR_CYCLES, 102 + HEAD_LEN, 2, lambda x: x),
(ATTR_DELTA_VOLTAGE, 112 + HEAD_LEN, 2, lambda x: float(x / 1000)),
(KEY_PROBLEM, 116 + HEAD_LEN, 8, lambda x: x),
]

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down Expand Up @@ -154,9 +156,11 @@ async def _async_update(self) -> BMSsample:

data |= {
key: func(
int.from_bytes(self._data[idx : idx + 2], byteorder="big", signed=True)
int.from_bytes(
self._data[idx : idx + size], byteorder="big", signed=True
)
)
for key, idx, func in BMS._FIELDS
for key, idx, size, func in BMS._FIELDS
}

# get temperatures
Expand Down
2 changes: 2 additions & 0 deletions custom_components/bms_ble/plugins/dpwrcore_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ATTR_VOLTAGE,
KEY_CELL_COUNT,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
)

from .basebms import BaseBMS, BMSsample
Expand Down Expand Up @@ -59,6 +60,7 @@ class BMS(BaseBMS):
),
(KEY_CELL_COUNT, Cmd.CELLVOLT, 6, 1, lambda x: min(x, BMS._MAX_CELLS)),
(ATTR_CYCLES, Cmd.LEGINFO2, 8, 2, lambda x: x),
(KEY_PROBLEM, Cmd.LEGINFO1, 15, 1, lambda x: x & 0xFF),
]

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down
2 changes: 2 additions & 0 deletions custom_components/bms_ble/plugins/ective_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
)

from .basebms import BaseBMS, BMSsample
Expand All @@ -41,6 +42,7 @@ class BMS(BaseBMS):
(ATTR_CYCLE_CHRG, 17, 8, False, lambda x: float(x / 1000)),
(ATTR_CYCLES, 25, 4, False, lambda x: x),
(ATTR_TEMPERATURE, 33, 4, False, lambda x: round(x * 0.1 - 273.15, 1)),
(KEY_PROBLEM, 37, 2, False, lambda x: x),
]

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down
7 changes: 7 additions & 0 deletions custom_components/bms_ble/plugins/ej_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
)

from .basebms import BaseBMS, BMSsample
Expand All @@ -45,6 +46,7 @@ class BMS(BaseBMS):
(ATTR_CYCLE_CHRG, Cmd.CAP, 15, 4, lambda x: float(x) / 10),
(ATTR_TEMPERATURE, Cmd.RT, 97, 2, lambda x: x - 40), # only 1st sensor relevant
(ATTR_CYCLES, Cmd.RT, 115, 4, lambda x: x),
(KEY_PROBLEM, Cmd.RT, 105, 4, lambda x: x & 0x0FFC), # mask status bits
]

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down Expand Up @@ -182,6 +184,11 @@ async def _async_update(self) -> BMSsample:
raw_data[Cmd.CAP] = bytearray(15) + self._data_final[125:]
break

if len(raw_data) != len(list(Cmd)) or not all(
len(value) > 0 for value in raw_data.values()
):
return {}

return {
key: func(int(raw_data[cmd.value][idx : idx + size], 16))
for key, cmd, idx, size, func in BMS._FIELDS
Expand Down
7 changes: 5 additions & 2 deletions custom_components/bms_ble/plugins/felicity_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
# KEY_PROBLEM, # TODO: enable
KEY_PROBLEM,
KEY_TEMP_VALUE,
)

Expand Down Expand Up @@ -145,5 +145,8 @@ async def _async_update(self) -> BMSsample:
BMS._decode_data(self._data_final)
| BMS._temp_sensors(self._data_final)
| BMS._cell_voltages(self._data_final)
# | {KEY_PROBLEM: self._data_final.get("Bwarn", 0) + self._data_final.get("Bfault", 0)}
| {
KEY_PROBLEM: self._data_final.get("Bwarn", 0)
+ self._data_final.get("Bfault", 0)
}
)
2 changes: 2 additions & 0 deletions custom_components/bms_ble/plugins/jbd_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
KEY_TEMP_SENS,
KEY_TEMP_VALUE,
)
Expand All @@ -41,6 +42,7 @@ class BMS(BaseBMS):
(ATTR_BATTERY_LEVEL, 23, 1, False, lambda x: x),
(ATTR_CYCLE_CHRG, 8, 2, False, lambda x: float(x / 100)),
(ATTR_CYCLES, 12, 2, False, lambda x: x),
(KEY_PROBLEM, 20, 2, False, lambda x: x),
] # general protocol v4

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down
11 changes: 11 additions & 0 deletions custom_components/bms_ble/plugins/jikong_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ATTR_VOLTAGE,
KEY_CELL_COUNT,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
KEY_TEMP_VALUE,
)

Expand All @@ -45,6 +46,7 @@ class BMS(BaseBMS):
(ATTR_CYCLE_CHRG, 174, 4, False, lambda x: float(x / 1000)),
(ATTR_CYCLES, 182, 4, False, lambda x: x),
(ATTR_BALANCE_CUR, 170, 2, True, lambda x: float(x / 1000)),
(KEY_PROBLEM, 166, 4, False, lambda x: x),
]
)

Expand Down Expand Up @@ -283,6 +285,15 @@ async def _async_update(self) -> BMSsample:
)

data: BMSsample = self._decode_data(self._data_final, self._prot_offset)
data.update(
{
KEY_PROBLEM: (
(int(data[KEY_PROBLEM]) >> 16)
if self._prot_offset
else (int(data[KEY_PROBLEM]) & 0xFFFF)
)
}
)
data.update(BMS._temp_sensors(self._data_final, self._prot_offset))
data.update(BMS._cell_voltages(self._data_final, int(data[KEY_CELL_COUNT])))

Expand Down
2 changes: 2 additions & 0 deletions custom_components/bms_ble/plugins/redodo_bms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ATTR_TEMPERATURE,
ATTR_VOLTAGE,
KEY_CELL_VOLTAGE,
KEY_PROBLEM,
KEY_TEMP_VALUE,
)

Expand All @@ -39,6 +40,7 @@ class BMS(BaseBMS):
(ATTR_BATTERY_LEVEL, 90, 2, False, lambda x: x),
(ATTR_CYCLE_CHRG, 62, 2, False, lambda x: float(x / 100)),
(ATTR_CYCLES, 96, 4, False, lambda x: x),
(KEY_PROBLEM, 76, 4, False, lambda x: x),
]

def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
Expand Down
Loading