aiobmsble 0.2.3__tar.gz → 0.4__tar.gz
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.
- aiobmsble-0.4/PKG-INFO +163 -0
- aiobmsble-0.4/README.md +131 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/__init__.py +2 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/__main__.py +4 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/basebms.py +40 -15
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/abc_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/ant_bms.py +4 -3
- aiobmsble-0.4/aiobmsble/bms/ant_leg_bms.py +177 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/braunpwr_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/cbtpwr_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/cbtpwr_vb_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/daly_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/dpwrcore_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/dummy_bms.py +3 -3
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/ecoworthy_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/ective_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/ej_bms.py +9 -3
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/felicity_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/jbd_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/jikong_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/neey_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/ogt_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/pro_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/redodo_bms.py +5 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/renogy_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/renogy_pro_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/roypow_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/seplos_bms.py +3 -3
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/seplos_v2_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/tdt_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/tianpwr_bms.py +2 -2
- aiobmsble-0.4/aiobmsble/test_data/__init__.py +95 -0
- aiobmsble-0.4/aiobmsble/test_data/abc_bms.json +34 -0
- aiobmsble-0.4/aiobmsble/test_data/ant_bms.json +18 -0
- aiobmsble-0.4/aiobmsble/test_data/ant_leg_bms.json +19 -0
- aiobmsble-0.4/aiobmsble/test_data/braunpwr_bms.json +34 -0
- aiobmsble-0.4/aiobmsble/test_data/cbtpwr_bms.json +90 -0
- aiobmsble-0.4/aiobmsble/test_data/cbtpwr_vb_bms.json +20 -0
- aiobmsble-0.4/aiobmsble/test_data/daly_bms.json +100 -0
- aiobmsble-0.4/aiobmsble/test_data/dpwrcore_bms.json +18 -0
- aiobmsble-0.4/aiobmsble/test_data/ecoworthy_bms.json +100 -0
- aiobmsble-0.4/aiobmsble/test_data/ective_bms.json +104 -0
- aiobmsble-0.4/aiobmsble/test_data/ej_bms.json +107 -0
- aiobmsble-0.4/aiobmsble/test_data/felicity_bms.json +24 -0
- aiobmsble-0.4/aiobmsble/test_data/ignore.json +48 -0
- aiobmsble-0.4/aiobmsble/test_data/jbd_bms.json +440 -0
- aiobmsble-0.4/aiobmsble/test_data/jikong_bms.json +54 -0
- aiobmsble-0.4/aiobmsble/test_data/neey_bms.json +65 -0
- aiobmsble-0.4/aiobmsble/test_data/ogt_bms.json +16 -0
- aiobmsble-0.4/aiobmsble/test_data/pro_bms.json +15 -0
- aiobmsble-0.4/aiobmsble/test_data/redodo_bms.json +151 -0
- aiobmsble-0.4/aiobmsble/test_data/renogy_bms.json +19 -0
- aiobmsble-0.4/aiobmsble/test_data/renogy_pro_bms.json +16 -0
- aiobmsble-0.4/aiobmsble/test_data/roypow_bms.json +54 -0
- aiobmsble-0.4/aiobmsble/test_data/seplos_bms.json +96 -0
- aiobmsble-0.4/aiobmsble/test_data/seplos_v2_bms.json +41 -0
- aiobmsble-0.4/aiobmsble/test_data/tdt_bms.json +14 -0
- aiobmsble-0.4/aiobmsble/test_data/tianpwr_bms.json +13 -0
- aiobmsble-0.4/aiobmsble.egg-info/PKG-INFO +163 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble.egg-info/SOURCES.txt +29 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble.egg-info/requires.txt +2 -6
- {aiobmsble-0.2.3 → aiobmsble-0.4}/pyproject.toml +8 -12
- {aiobmsble-0.2.3 → aiobmsble-0.4}/tests/test_basebms.py +53 -4
- {aiobmsble-0.2.3 → aiobmsble-0.4}/tests/test_fuzzing.py +1 -1
- {aiobmsble-0.2.3 → aiobmsble-0.4}/tests/test_main.py +1 -1
- {aiobmsble-0.2.3 → aiobmsble-0.4}/tests/test_plugins.py +5 -7
- aiobmsble-0.4/tests/test_test_data.py +42 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/tests/test_utils.py +7 -7
- aiobmsble-0.2.3/PKG-INFO +0 -122
- aiobmsble-0.2.3/README.md +0 -86
- aiobmsble-0.2.3/aiobmsble.egg-info/PKG-INFO +0 -122
- {aiobmsble-0.2.3 → aiobmsble-0.4}/LICENSE +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/MANIFEST.in +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/bms/__init__.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble/utils.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble.egg-info/dependency_links.txt +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble.egg-info/entry_points.txt +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/aiobmsble.egg-info/top_level.txt +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/setup.cfg +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.4}/tests/test_examples.py +0 -0
aiobmsble-0.4/PKG-INFO
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: aiobmsble
|
3
|
+
Version: 0.4
|
4
|
+
Summary: Asynchronous Python library to query battery management systems via Bluetooth Low Energy.
|
5
|
+
Author: Patrick Loschmidt
|
6
|
+
Maintainer: Patrick Loschmidt
|
7
|
+
License-Expression: Apache-2.0
|
8
|
+
Project-URL: Homepage, https://github.com/patman15/aiobmsble/
|
9
|
+
Project-URL: Documentation, https://github.com/patman15/aiobmsble/README
|
10
|
+
Project-URL: Source Code, https://github.com/patman15/aiobmsble/
|
11
|
+
Project-URL: Bug Reports, https://github.com/patman15/aiobmsble/issues
|
12
|
+
Keywords: BMS,BLE,battery,bluetooth
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Operating System :: OS Independent
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
16
|
+
Classifier: Intended Audience :: Developers
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
18
|
+
Requires-Python: >=3.12
|
19
|
+
Description-Content-Type: text/markdown
|
20
|
+
License-File: LICENSE
|
21
|
+
Requires-Dist: bleak>=1.0.1
|
22
|
+
Requires-Dist: bleak-retry-connector>=4.0.2
|
23
|
+
Provides-Extra: dev
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
25
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
26
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
27
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
28
|
+
Requires-Dist: hypothesis; extra == "dev"
|
29
|
+
Requires-Dist: mypy; extra == "dev"
|
30
|
+
Requires-Dist: ruff<0.14.0,>=0.12.1; extra == "dev"
|
31
|
+
Dynamic: license-file
|
32
|
+
|
33
|
+
[![GitHub Release][releases-shield]](https://pypi.org/p/aiobmsble/)
|
34
|
+
[![License][license-shield]](LICENSE)
|
35
|
+
|
36
|
+
# Aiobmsble
|
37
|
+
Requires Python 3 and uses [asyncio](https://pypi.org/project/asyncio/) and [bleak](https://pypi.org/project/bleak/)
|
38
|
+
|
39
|
+
## Asynchronous Library to Query Battery Management Systems via Bluetooth LE
|
40
|
+
This library is intended to query data from battery management systems that use Bluetooth LE. This library can be used stand-alone in any Python environment (with necessary dependencies installed). It is developed to support [BMS_BLE-HA integration](https://github.com/patman15/BMS_BLE-HA/) that was written to make BMS data available to Home Assistant, but can be hopefully usefull for other use-cases as well.
|
41
|
+
|
42
|
+
* [Features](#features)
|
43
|
+
* [Usage](#usage)
|
44
|
+
* [Installation](#installation)
|
45
|
+
* [Troubleshooting](#troubleshooting)
|
46
|
+
|
47
|
+
## Features
|
48
|
+
- Support for autodetecting compatible BLE BMSs
|
49
|
+
- Automatic detection of compatible BLE write mode
|
50
|
+
- Asynchronous operation using [asyncio](https://pypi.org/project/asyncio/)
|
51
|
+
- Any number of batteries in parallel
|
52
|
+
- 100% test coverage plus fuzz tests for BLE data
|
53
|
+
|
54
|
+
### Supported Devices
|
55
|
+
The [list of supported devices](https://github.com/patman15/BMS_BLE-HA/blob/feature-aiobmsble/README.md#supported-devices) is maintained in the repository of the related [Home Assistant integration](https://github.com/patman15/BMS_BLE-HA).
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
In order to identify all devices that are reachable and supported by the library, simply run
|
59
|
+
```bash
|
60
|
+
aiobmsble
|
61
|
+
```
|
62
|
+
from the command line after [installation](#installation).
|
63
|
+
|
64
|
+
### From your Python code
|
65
|
+
In case you need a reference to include the code into your library, please see [\_\_main\_\_.py](/aiobmsble/__main__.py).
|
66
|
+
|
67
|
+
### From a Script
|
68
|
+
This example can also be found as an [example](/examples/minimal.py) in the respective [folder](/main/examples).
|
69
|
+
```python
|
70
|
+
"""Example of using the aiobmsble library to find a BLE device by name and print its sensor data.
|
71
|
+
|
72
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
73
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
74
|
+
"""
|
75
|
+
|
76
|
+
import asyncio
|
77
|
+
import logging
|
78
|
+
from typing import Final
|
79
|
+
|
80
|
+
from bleak import BleakScanner
|
81
|
+
from bleak.backends.device import BLEDevice
|
82
|
+
from bleak.exc import BleakError
|
83
|
+
|
84
|
+
from aiobmsble import BMSsample
|
85
|
+
from aiobmsble.bms.dummy_bms import BMS # TODO: use the right BMS class for your device
|
86
|
+
|
87
|
+
NAME: Final[str] = "BT Device Name" # TODO: replace with the name of your BLE device
|
88
|
+
|
89
|
+
# Configure logging
|
90
|
+
logging.basicConfig(level=logging.INFO)
|
91
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
92
|
+
|
93
|
+
|
94
|
+
async def main(dev_name) -> None:
|
95
|
+
"""Find a BLE device by name and update its sensor data."""
|
96
|
+
|
97
|
+
device: BLEDevice | None = await BleakScanner.find_device_by_name(dev_name)
|
98
|
+
if device is None:
|
99
|
+
logger.error("Device '%s' not found.", dev_name)
|
100
|
+
return
|
101
|
+
|
102
|
+
logger.info("Found device: %s (%s)", device.name, device.address)
|
103
|
+
try:
|
104
|
+
async with BMS(ble_device=device) as bms:
|
105
|
+
logger.info("Updating BMS data...")
|
106
|
+
data: BMSsample = await bms.async_update()
|
107
|
+
logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
|
108
|
+
except BleakError as ex:
|
109
|
+
logger.error("Failed to update BMS: %s", type(ex).__name__)
|
110
|
+
|
111
|
+
|
112
|
+
if __name__ == "__main__":
|
113
|
+
asyncio.run(main(NAME)) # pragma: no cover
|
114
|
+
```
|
115
|
+
|
116
|
+
### Testing
|
117
|
+
For integrations tests (using pytest) the library provides advertisement data that can be used to verify detection of BMSs. To use it, include the following line into your `conftest.py`:
|
118
|
+
```python
|
119
|
+
pytest_plugins: list[str] = ["aiobmsble.test_data"]
|
120
|
+
```
|
121
|
+
|
122
|
+
For your tests you can then use
|
123
|
+
```python
|
124
|
+
def test_advertisements(bms_advertisements) -> None:
|
125
|
+
"""Run some tests with the advertisements"""
|
126
|
+
for advertisement, bms_type, _comments in bms_advertisements:
|
127
|
+
...
|
128
|
+
```
|
129
|
+
|
130
|
+
## Installation
|
131
|
+
Install python and pip if you have not already, then run:
|
132
|
+
```bash
|
133
|
+
pip3 install pip --upgrade
|
134
|
+
pip3 install wheel
|
135
|
+
```
|
136
|
+
|
137
|
+
### For Production:
|
138
|
+
|
139
|
+
```bash
|
140
|
+
pip3 install aiobmsble
|
141
|
+
```
|
142
|
+
This will install the latest library release and all of it's python dependencies.
|
143
|
+
|
144
|
+
### For Development:
|
145
|
+
```bash
|
146
|
+
git clone https://github.com/patman15/aiobmsble.git
|
147
|
+
cd aiobmsble
|
148
|
+
pip3 install -e .[dev]
|
149
|
+
```
|
150
|
+
This gives you the latest library code from the main branch.
|
151
|
+
|
152
|
+
## Troubleshooting
|
153
|
+
In case you have problems with the library, please enable debug logging. You can also run `aiobmsble -v` from the command line in order to query all known BMS that are reachable.
|
154
|
+
|
155
|
+
### In case you have troubles you'd like to have help with
|
156
|
+
|
157
|
+
- please record a debug log using `aiobmsble -v -l debug.log`,
|
158
|
+
- [open an issue](https://github.com/patman15/aiobmsble/issues/new?assignees=&labels=question&projects=&template=support.yml) with a good description of what your question/issue is and attach the log, or
|
159
|
+
- [open a bug](https://github.com/patman15/aiobmsble/issues/new?assignees=&labels=Bug&projects=&template=bug.yml) if you think the behaviour you see is misbehaviour of the library, including a good description of what happened, your expectations,
|
160
|
+
- and put the `debug.log` **as attachement** to the issue.
|
161
|
+
|
162
|
+
[license-shield]: https://img.shields.io/github/license/patman15/aiobmsble?style=for-the-badge&cacheSeconds=86400
|
163
|
+
[releases-shield]: https://img.shields.io/pypi/v/aiobmsble?style=for-the-badge
|
aiobmsble-0.4/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
[![GitHub Release][releases-shield]](https://pypi.org/p/aiobmsble/)
|
2
|
+
[![License][license-shield]](LICENSE)
|
3
|
+
|
4
|
+
# Aiobmsble
|
5
|
+
Requires Python 3 and uses [asyncio](https://pypi.org/project/asyncio/) and [bleak](https://pypi.org/project/bleak/)
|
6
|
+
|
7
|
+
## Asynchronous Library to Query Battery Management Systems via Bluetooth LE
|
8
|
+
This library is intended to query data from battery management systems that use Bluetooth LE. This library can be used stand-alone in any Python environment (with necessary dependencies installed). It is developed to support [BMS_BLE-HA integration](https://github.com/patman15/BMS_BLE-HA/) that was written to make BMS data available to Home Assistant, but can be hopefully usefull for other use-cases as well.
|
9
|
+
|
10
|
+
* [Features](#features)
|
11
|
+
* [Usage](#usage)
|
12
|
+
* [Installation](#installation)
|
13
|
+
* [Troubleshooting](#troubleshooting)
|
14
|
+
|
15
|
+
## Features
|
16
|
+
- Support for autodetecting compatible BLE BMSs
|
17
|
+
- Automatic detection of compatible BLE write mode
|
18
|
+
- Asynchronous operation using [asyncio](https://pypi.org/project/asyncio/)
|
19
|
+
- Any number of batteries in parallel
|
20
|
+
- 100% test coverage plus fuzz tests for BLE data
|
21
|
+
|
22
|
+
### Supported Devices
|
23
|
+
The [list of supported devices](https://github.com/patman15/BMS_BLE-HA/blob/feature-aiobmsble/README.md#supported-devices) is maintained in the repository of the related [Home Assistant integration](https://github.com/patman15/BMS_BLE-HA).
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
In order to identify all devices that are reachable and supported by the library, simply run
|
27
|
+
```bash
|
28
|
+
aiobmsble
|
29
|
+
```
|
30
|
+
from the command line after [installation](#installation).
|
31
|
+
|
32
|
+
### From your Python code
|
33
|
+
In case you need a reference to include the code into your library, please see [\_\_main\_\_.py](/aiobmsble/__main__.py).
|
34
|
+
|
35
|
+
### From a Script
|
36
|
+
This example can also be found as an [example](/examples/minimal.py) in the respective [folder](/main/examples).
|
37
|
+
```python
|
38
|
+
"""Example of using the aiobmsble library to find a BLE device by name and print its sensor data.
|
39
|
+
|
40
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
41
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
42
|
+
"""
|
43
|
+
|
44
|
+
import asyncio
|
45
|
+
import logging
|
46
|
+
from typing import Final
|
47
|
+
|
48
|
+
from bleak import BleakScanner
|
49
|
+
from bleak.backends.device import BLEDevice
|
50
|
+
from bleak.exc import BleakError
|
51
|
+
|
52
|
+
from aiobmsble import BMSsample
|
53
|
+
from aiobmsble.bms.dummy_bms import BMS # TODO: use the right BMS class for your device
|
54
|
+
|
55
|
+
NAME: Final[str] = "BT Device Name" # TODO: replace with the name of your BLE device
|
56
|
+
|
57
|
+
# Configure logging
|
58
|
+
logging.basicConfig(level=logging.INFO)
|
59
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
60
|
+
|
61
|
+
|
62
|
+
async def main(dev_name) -> None:
|
63
|
+
"""Find a BLE device by name and update its sensor data."""
|
64
|
+
|
65
|
+
device: BLEDevice | None = await BleakScanner.find_device_by_name(dev_name)
|
66
|
+
if device is None:
|
67
|
+
logger.error("Device '%s' not found.", dev_name)
|
68
|
+
return
|
69
|
+
|
70
|
+
logger.info("Found device: %s (%s)", device.name, device.address)
|
71
|
+
try:
|
72
|
+
async with BMS(ble_device=device) as bms:
|
73
|
+
logger.info("Updating BMS data...")
|
74
|
+
data: BMSsample = await bms.async_update()
|
75
|
+
logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
|
76
|
+
except BleakError as ex:
|
77
|
+
logger.error("Failed to update BMS: %s", type(ex).__name__)
|
78
|
+
|
79
|
+
|
80
|
+
if __name__ == "__main__":
|
81
|
+
asyncio.run(main(NAME)) # pragma: no cover
|
82
|
+
```
|
83
|
+
|
84
|
+
### Testing
|
85
|
+
For integrations tests (using pytest) the library provides advertisement data that can be used to verify detection of BMSs. To use it, include the following line into your `conftest.py`:
|
86
|
+
```python
|
87
|
+
pytest_plugins: list[str] = ["aiobmsble.test_data"]
|
88
|
+
```
|
89
|
+
|
90
|
+
For your tests you can then use
|
91
|
+
```python
|
92
|
+
def test_advertisements(bms_advertisements) -> None:
|
93
|
+
"""Run some tests with the advertisements"""
|
94
|
+
for advertisement, bms_type, _comments in bms_advertisements:
|
95
|
+
...
|
96
|
+
```
|
97
|
+
|
98
|
+
## Installation
|
99
|
+
Install python and pip if you have not already, then run:
|
100
|
+
```bash
|
101
|
+
pip3 install pip --upgrade
|
102
|
+
pip3 install wheel
|
103
|
+
```
|
104
|
+
|
105
|
+
### For Production:
|
106
|
+
|
107
|
+
```bash
|
108
|
+
pip3 install aiobmsble
|
109
|
+
```
|
110
|
+
This will install the latest library release and all of it's python dependencies.
|
111
|
+
|
112
|
+
### For Development:
|
113
|
+
```bash
|
114
|
+
git clone https://github.com/patman15/aiobmsble.git
|
115
|
+
cd aiobmsble
|
116
|
+
pip3 install -e .[dev]
|
117
|
+
```
|
118
|
+
This gives you the latest library code from the main branch.
|
119
|
+
|
120
|
+
## Troubleshooting
|
121
|
+
In case you have problems with the library, please enable debug logging. You can also run `aiobmsble -v` from the command line in order to query all known BMS that are reachable.
|
122
|
+
|
123
|
+
### In case you have troubles you'd like to have help with
|
124
|
+
|
125
|
+
- please record a debug log using `aiobmsble -v -l debug.log`,
|
126
|
+
- [open an issue](https://github.com/patman15/aiobmsble/issues/new?assignees=&labels=question&projects=&template=support.yml) with a good description of what your question/issue is and attach the log, or
|
127
|
+
- [open a bug](https://github.com/patman15/aiobmsble/issues/new?assignees=&labels=Bug&projects=&template=bug.yml) if you think the behaviour you see is misbehaviour of the library, including a good description of what happened, your expectations,
|
128
|
+
- and put the `debug.log` **as attachement** to the issue.
|
129
|
+
|
130
|
+
[license-shield]: https://img.shields.io/github/license/patman15/aiobmsble?style=for-the-badge&cacheSeconds=86400
|
131
|
+
[releases-shield]: https://img.shields.io/pypi/v/aiobmsble?style=for-the-badge
|
@@ -19,6 +19,7 @@ type BMSvalue = Literal[
|
|
19
19
|
"cycles",
|
20
20
|
"cycle_capacity",
|
21
21
|
"cycle_charge",
|
22
|
+
"total_charge",
|
22
23
|
"delta_voltage",
|
23
24
|
"problem",
|
24
25
|
"runtime",
|
@@ -69,6 +70,7 @@ class BMSsample(TypedDict, total=False):
|
|
69
70
|
cell_count: int # [#]
|
70
71
|
cell_voltages: list[float] # [V]
|
71
72
|
cycle_charge: int | float # [Ah]
|
73
|
+
total_charge: int # [Ah], overall discharged
|
72
74
|
design_capacity: int # [Ah]
|
73
75
|
pack_count: int # [#]
|
74
76
|
temp_sensors: int # [#]
|
@@ -31,7 +31,7 @@ async def scan_devices() -> dict[str, tuple[BLEDevice, AdvertisementData]]:
|
|
31
31
|
scan_result: dict[str, tuple[BLEDevice, AdvertisementData]] = (
|
32
32
|
await BleakScanner.discover(return_adv=True)
|
33
33
|
)
|
34
|
-
logger.
|
34
|
+
logger.debug(scan_result)
|
35
35
|
logger.info("%i BT devices in range.", len(scan_result))
|
36
36
|
return scan_result
|
37
37
|
|
@@ -51,7 +51,7 @@ async def detect_bms() -> None:
|
|
51
51
|
|
52
52
|
if bms_cls := bms_identify(advertisement):
|
53
53
|
logger.info("Found matching BMS type: %s", bms_cls.device_id())
|
54
|
-
bms: BaseBMS = bms_cls(ble_device=ble_dev
|
54
|
+
bms: BaseBMS = bms_cls(ble_device=ble_dev)
|
55
55
|
|
56
56
|
try:
|
57
57
|
logger.info("Updating BMS data...")
|
@@ -59,6 +59,8 @@ async def detect_bms() -> None:
|
|
59
59
|
logger.info("BMS data: %s", repr(data).replace(", '", ",\n\t'"))
|
60
60
|
except (BleakError, TimeoutError) as exc:
|
61
61
|
logger.error("Failed to update BMS: %s", type(exc).__name__)
|
62
|
+
finally:
|
63
|
+
await bms.disconnect()
|
62
64
|
|
63
65
|
logger.info("done.")
|
64
66
|
|
@@ -9,7 +9,8 @@ import asyncio
|
|
9
9
|
from collections.abc import Callable, MutableMapping
|
10
10
|
import logging
|
11
11
|
from statistics import fmean
|
12
|
-
from
|
12
|
+
from types import TracebackType
|
13
|
+
from typing import Any, Final, Literal, Self
|
13
14
|
|
14
15
|
from bleak import BleakClient
|
15
16
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
@@ -44,26 +45,28 @@ class BaseBMS(ABC):
|
|
44
45
|
def __init__(
|
45
46
|
self,
|
46
47
|
ble_device: BLEDevice,
|
47
|
-
|
48
|
+
keep_alive: bool = True,
|
48
49
|
logger_name: str = "",
|
49
50
|
) -> None:
|
50
51
|
"""Intialize the BMS.
|
51
52
|
|
52
|
-
|
53
|
+
`_notification_handler`: the callback function used for notifications from `uuid_rx()`
|
53
54
|
characteristic. Not defined as abstract in this base class, as it can be both,
|
54
55
|
a normal or async function
|
55
56
|
|
56
57
|
Args:
|
57
|
-
logger_name (str): name of the logger for the BMS instance (usually file name)
|
58
58
|
ble_device (BLEDevice): the Bleak device to connect to
|
59
|
-
|
59
|
+
keep_alive (bool): if true, the connection will be kept active after each update.
|
60
|
+
Make sure to call `disconnect()` when done using the BMS class or better use
|
61
|
+
`async with` context manager (requires `keep_alive=True`).
|
62
|
+
logger_name (str): name of the logger for the BMS instance, default: module name
|
60
63
|
|
61
64
|
"""
|
62
65
|
assert (
|
63
66
|
getattr(self, "_notification_handler", None) is not None
|
64
67
|
), "BMS class must define _notification_handler method"
|
65
68
|
self._ble_device: Final[BLEDevice] = ble_device
|
66
|
-
self.
|
69
|
+
self._keep_alive: Final[bool] = keep_alive
|
67
70
|
self.name: Final[str] = self._ble_device.name or "undefined"
|
68
71
|
self._inv_wr_mode: bool | None = None # invert write mode (WNR <-> W)
|
69
72
|
logger_name = logger_name or self.__class__.__module__
|
@@ -83,6 +86,22 @@ class BaseBMS(ABC):
|
|
83
86
|
self._data: bytearray = bytearray()
|
84
87
|
self._data_event: Final[asyncio.Event] = asyncio.Event()
|
85
88
|
|
89
|
+
async def __aenter__(self) -> Self:
|
90
|
+
"""Asynchronous context manager to implement `async with` functionality."""
|
91
|
+
if not self._keep_alive:
|
92
|
+
raise ValueError("usage of context manager requires `keep_alive=True`.")
|
93
|
+
await self._connect()
|
94
|
+
return self
|
95
|
+
|
96
|
+
async def __aexit__(
|
97
|
+
self,
|
98
|
+
typ: type[BaseException] | None,
|
99
|
+
exc: BaseException | None,
|
100
|
+
tb: TracebackType | None,
|
101
|
+
) -> None:
|
102
|
+
"""Asynchronous context manager exit functionality."""
|
103
|
+
await self.disconnect()
|
104
|
+
|
86
105
|
@classmethod
|
87
106
|
def get_bms_module(cls) -> str:
|
88
107
|
"""Return BMS module name, e.g. aiobmsble.bms.dummy_bms."""
|
@@ -177,6 +196,10 @@ class BaseBMS(ABC):
|
|
177
196
|
{"voltage", "cycle_charge"},
|
178
197
|
lambda: round(data.get("voltage", 0) * data.get("cycle_charge", 0), 3),
|
179
198
|
),
|
199
|
+
"cycles": (
|
200
|
+
{"design_capacity", "total_charge"},
|
201
|
+
lambda: data.get("total_charge", 0) // data.get("design_capacity", 0),
|
202
|
+
),
|
180
203
|
"power": (
|
181
204
|
{"voltage", "current"},
|
182
205
|
lambda: round(data.get("voltage", 0) * current, 3),
|
@@ -356,8 +379,8 @@ class BaseBMS(ABC):
|
|
356
379
|
if reset:
|
357
380
|
self._inv_wr_mode = None # reset write mode
|
358
381
|
await self._client.disconnect()
|
359
|
-
except BleakError:
|
360
|
-
self._log.
|
382
|
+
except (BleakError, TimeoutError) as exc:
|
383
|
+
self._log.error("disconnect failed! (%s)", type(exc).__name__)
|
361
384
|
|
362
385
|
async def _wait_event(self) -> None:
|
363
386
|
"""Wait for data event and clear it."""
|
@@ -384,7 +407,7 @@ class BaseBMS(ABC):
|
|
384
407
|
data: BMSsample = await self._async_update()
|
385
408
|
self._add_missing_values(data, self._calc_values())
|
386
409
|
|
387
|
-
if self.
|
410
|
+
if not self._keep_alive:
|
388
411
|
# disconnect after data update to force reconnect next time (slow!)
|
389
412
|
await self.disconnect()
|
390
413
|
|
@@ -478,16 +501,18 @@ class BaseBMS(ABC):
|
|
478
501
|
|
479
502
|
"""
|
480
503
|
return [
|
481
|
-
value
|
504
|
+
(value - offset) / divider
|
482
505
|
for idx in range(values)
|
483
506
|
if (len(data) >= start + (idx + 1) * size)
|
484
507
|
and (
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
508
|
+
(
|
509
|
+
value := int.from_bytes(
|
510
|
+
data[start + idx * size : start + (idx + 1) * size],
|
511
|
+
byteorder=byteorder,
|
512
|
+
signed=signed,
|
513
|
+
)
|
489
514
|
)
|
490
|
-
|
515
|
+
or (offset == 0)
|
491
516
|
)
|
492
517
|
]
|
493
518
|
|
@@ -47,9 +47,9 @@ class BMS(BaseBMS):
|
|
47
47
|
)
|
48
48
|
_RESPS: Final[set[int]] = {field.idx for field in _FIELDS} | {0xF4} # cell voltages
|
49
49
|
|
50
|
-
def __init__(self, ble_device: BLEDevice,
|
50
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
51
51
|
"""Initialize BMS."""
|
52
|
-
super().__init__(ble_device,
|
52
|
+
super().__init__(ble_device, keep_alive)
|
53
53
|
self._data_final: dict[int, bytearray] = {}
|
54
54
|
self._exp_reply: set[int] = set()
|
55
55
|
|
@@ -41,13 +41,14 @@ class BMS(BaseBMS):
|
|
41
41
|
| ((x & 0xF) if (x & 0xF) not in (0x1, 0x4, 0xB, 0xC, 0xF) else 0),
|
42
42
|
),
|
43
43
|
BMSdp("cycle_charge", 54, 4, False, lambda x: x / 1e6),
|
44
|
+
BMSdp("total_charge", 58, 4, False, lambda x: x // 1000),
|
44
45
|
BMSdp("delta_voltage", 82, 2, False, lambda x: x / 1000),
|
45
46
|
BMSdp("power", 62, 4, True, lambda x: x / 1),
|
46
47
|
)
|
47
48
|
|
48
|
-
def __init__(self, ble_device: BLEDevice,
|
49
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
49
50
|
"""Initialize BMS."""
|
50
|
-
super().__init__(ble_device,
|
51
|
+
super().__init__(ble_device, keep_alive)
|
51
52
|
self._data_final: bytearray = bytearray()
|
52
53
|
self._valid_reply: int = BMS._CMD_STAT | 0x10 # valid reply mask
|
53
54
|
self._exp_len: int = BMS._MIN_LEN
|
@@ -87,7 +88,7 @@ class BMS(BaseBMS):
|
|
87
88
|
@staticmethod
|
88
89
|
def _calc_values() -> frozenset[BMSvalue]:
|
89
90
|
return frozenset(
|
90
|
-
{"cycle_capacity", "temperature"}
|
91
|
+
{"cycle_capacity", "cycles", "temperature"}
|
91
92
|
) # calculate further values from BMS provided set ones
|
92
93
|
|
93
94
|
async def _init_connection(
|
@@ -0,0 +1,177 @@
|
|
1
|
+
"""Module to support ANT BMS."""
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
from enum import IntEnum
|
5
|
+
from typing import Final, override
|
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, crc_sum
|
13
|
+
|
14
|
+
|
15
|
+
class BMS(BaseBMS):
|
16
|
+
"""ANT BMS (legacy) implementation."""
|
17
|
+
|
18
|
+
class CMD(IntEnum):
|
19
|
+
"""Command codes for ANT BMS."""
|
20
|
+
|
21
|
+
GET = 0xDB
|
22
|
+
SET = 0xA5
|
23
|
+
|
24
|
+
class ADR(IntEnum):
|
25
|
+
"""Address codes for ANT BMS."""
|
26
|
+
|
27
|
+
STATUS = 0x00
|
28
|
+
|
29
|
+
_RX_HEADER: Final[bytes] = b"\xaa\x55\xaa"
|
30
|
+
_RX_HEADER_RSP_STAT: Final[bytes] = b"\xaa\x55\xaa\xff"
|
31
|
+
|
32
|
+
_RSP_STAT: Final[int] = 0xFF
|
33
|
+
_RSP_STAT_LEN: Final[int] = 140
|
34
|
+
|
35
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
36
|
+
BMSdp("voltage", 4, 2, False, lambda x: x / 10),
|
37
|
+
BMSdp("current", 70, 4, True, lambda x: x / -10),
|
38
|
+
BMSdp("battery_level", 74, 1, False),
|
39
|
+
BMSdp("design_capacity", 75, 4, False, lambda x: x // 1e6),
|
40
|
+
BMSdp("cycle_charge", 79, 4, False, lambda x: x / 1e6),
|
41
|
+
BMSdp("total_charge", 83, 4, False, lambda x: x // 1000),
|
42
|
+
BMSdp("runtime", 87, 4, False),
|
43
|
+
BMSdp("cell_count", 123, 1, False),
|
44
|
+
)
|
45
|
+
|
46
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
47
|
+
"""Initialize BMS."""
|
48
|
+
super().__init__(ble_device, keep_alive)
|
49
|
+
self._data_final: bytearray
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
@override
|
53
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
54
|
+
"""Provide BluetoothMatcher definition."""
|
55
|
+
return [
|
56
|
+
{
|
57
|
+
"local_name": "ANT-BLE*",
|
58
|
+
"service_uuid": BMS.uuid_services()[0],
|
59
|
+
"manufacturer_id": 1623,
|
60
|
+
"connectable": True,
|
61
|
+
}
|
62
|
+
]
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
@override
|
66
|
+
def device_info() -> dict[str, str]:
|
67
|
+
"""Return device information for the battery management system."""
|
68
|
+
return {"manufacturer": "ANT", "model": "Smart BMS"}
|
69
|
+
|
70
|
+
@staticmethod
|
71
|
+
@override
|
72
|
+
def uuid_services() -> list[str]:
|
73
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
74
|
+
return [normalize_uuid_str("ffe0")] # change service UUID here!
|
75
|
+
|
76
|
+
@staticmethod
|
77
|
+
@override
|
78
|
+
def uuid_rx() -> str:
|
79
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
80
|
+
return "ffe1"
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
@override
|
84
|
+
def uuid_tx() -> str:
|
85
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
86
|
+
return "ffe1"
|
87
|
+
|
88
|
+
@staticmethod
|
89
|
+
@override
|
90
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
91
|
+
return frozenset(
|
92
|
+
(
|
93
|
+
"battery_charging",
|
94
|
+
"cycle_capacity",
|
95
|
+
"cycles",
|
96
|
+
"delta_voltage",
|
97
|
+
"power",
|
98
|
+
"temperature",
|
99
|
+
)
|
100
|
+
) # calculate further values from BMS provided set ones
|
101
|
+
|
102
|
+
def _notification_handler(
|
103
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
104
|
+
) -> None:
|
105
|
+
"""Handle the RX characteristics notify event (new data arrives)."""
|
106
|
+
|
107
|
+
self._log.debug("RX BLE data: %s", data)
|
108
|
+
|
109
|
+
if data.startswith(BMS._RX_HEADER_RSP_STAT):
|
110
|
+
self._data = bytearray()
|
111
|
+
elif not self._data:
|
112
|
+
self._log.debug("invalid start of frame")
|
113
|
+
return
|
114
|
+
|
115
|
+
self._data += data
|
116
|
+
|
117
|
+
_data_len: Final[int] = len(self._data)
|
118
|
+
if _data_len < BMS._RSP_STAT_LEN:
|
119
|
+
return
|
120
|
+
|
121
|
+
if _data_len > BMS._RSP_STAT_LEN:
|
122
|
+
self._log.debug("invalid length %d > %d", _data_len, BMS._RSP_STAT_LEN)
|
123
|
+
self._data.clear()
|
124
|
+
return
|
125
|
+
|
126
|
+
if (local_crc := crc_sum(self._data[4:-2], 2)) != (
|
127
|
+
remote_crc := int.from_bytes(self._data[-2:], byteorder="big", signed=False)
|
128
|
+
):
|
129
|
+
self._log.debug("invalid checksum 0x%X != 0x%X", local_crc, remote_crc)
|
130
|
+
self._data.clear()
|
131
|
+
return
|
132
|
+
|
133
|
+
self._data_final = self._data.copy()
|
134
|
+
self._data.clear()
|
135
|
+
self._data_event.set()
|
136
|
+
|
137
|
+
@staticmethod
|
138
|
+
def _cmd(cmd: CMD, adr: ADR, value: int = 0x0000) -> bytes:
|
139
|
+
"""Assemble a ANT BMS command."""
|
140
|
+
_frame = bytearray((cmd, cmd, adr))
|
141
|
+
_frame += value.to_bytes(2, "big")
|
142
|
+
_frame += crc_sum(_frame[2:], 1).to_bytes(1, "big")
|
143
|
+
return bytes(_frame)
|
144
|
+
|
145
|
+
@override
|
146
|
+
async def _async_update(self) -> BMSsample:
|
147
|
+
"""Update battery status information."""
|
148
|
+
await self._await_reply(BMS._cmd(BMS.CMD.GET, BMS.ADR.STATUS))
|
149
|
+
|
150
|
+
_data: bytearray = self._data_final
|
151
|
+
result: BMSsample = BMS._decode_data(
|
152
|
+
BMS._FIELDS, _data, byteorder="big", offset=0
|
153
|
+
)
|
154
|
+
|
155
|
+
result["cell_voltages"] = BMS._cell_voltages(
|
156
|
+
_data,
|
157
|
+
cells=result["cell_count"],
|
158
|
+
start=6,
|
159
|
+
size=2,
|
160
|
+
byteorder="big",
|
161
|
+
divider=1000,
|
162
|
+
)
|
163
|
+
|
164
|
+
if not result["design_capacity"]:
|
165
|
+
# Workaround for some BMS always reporting 0 for design_capacity
|
166
|
+
result.pop("design_capacity")
|
167
|
+
with contextlib.suppress(ZeroDivisionError):
|
168
|
+
result["design_capacity"] = int(
|
169
|
+
round((result["cycle_charge"] / result["battery_level"]) * 100, -1)
|
170
|
+
) # leads to `cycles` not available when level == 0
|
171
|
+
|
172
|
+
# ANT-BMS carries 6 slots for temp sensors but only 4 looks like being connected by default
|
173
|
+
result["temp_values"] = BMS._temp_values(
|
174
|
+
_data, values=4, start=91, size=2, byteorder="big", signed=True
|
175
|
+
)
|
176
|
+
|
177
|
+
return result
|