bluetti-bt-lib 0.0.1__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.
- bluetti_bt_lib-0.0.1/LICENSE +21 -0
- bluetti_bt_lib-0.0.1/PKG-INFO +57 -0
- bluetti_bt_lib-0.0.1/README.md +27 -0
- bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/PKG-INFO +57 -0
- bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/SOURCES.txt +44 -0
- bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/dependency_links.txt +1 -0
- bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/entry_points.txt +2 -0
- bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/requires.txt +7 -0
- bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/top_level.txt +1 -0
- bluetti_bt_lib-0.0.1/setup.cfg +4 -0
- bluetti_bt_lib-0.0.1/setup.py +44 -0
- bluetti_bt_lib-0.0.1/src/__init__.py +2 -0
- bluetti_bt_lib-0.0.1/src/base_devices/BaseDeviceV1.py +29 -0
- bluetti_bt_lib-0.0.1/src/base_devices/BaseDeviceV2.py +25 -0
- bluetti_bt_lib-0.0.1/src/base_devices/BluettiDevice.py +49 -0
- bluetti_bt_lib-0.0.1/src/base_devices/__init__.py +3 -0
- bluetti_bt_lib-0.0.1/src/bluetooth/__init__.py +1 -0
- bluetti_bt_lib-0.0.1/src/bluetooth/device_reader.py +202 -0
- bluetti_bt_lib-0.0.1/src/bluetooth/device_recognizer.py +82 -0
- bluetti_bt_lib-0.0.1/src/bluetooth/encryption.py +296 -0
- bluetti_bt_lib-0.0.1/src/const.py +2 -0
- bluetti_bt_lib-0.0.1/src/devices/__init__.py +19 -0
- bluetti_bt_lib-0.0.1/src/devices/ac180.py +15 -0
- bluetti_bt_lib-0.0.1/src/devices/ac70.py +15 -0
- bluetti_bt_lib-0.0.1/src/devices/eb3a.py +5 -0
- bluetti_bt_lib-0.0.1/src/devices/handsfree1.py +15 -0
- bluetti_bt_lib-0.0.1/src/fields/BoolField.py +11 -0
- bluetti_bt_lib-0.0.1/src/fields/DecimalArrayField.py +14 -0
- bluetti_bt_lib-0.0.1/src/fields/DecimalField.py +15 -0
- bluetti_bt_lib-0.0.1/src/fields/DeviceField.py +13 -0
- bluetti_bt_lib-0.0.1/src/fields/EnumField.py +15 -0
- bluetti_bt_lib-0.0.1/src/fields/FieldName.py +12 -0
- bluetti_bt_lib-0.0.1/src/fields/IntField.py +11 -0
- bluetti_bt_lib-0.0.1/src/fields/StringField.py +9 -0
- bluetti_bt_lib-0.0.1/src/fields/SwapStringField.py +17 -0
- bluetti_bt_lib-0.0.1/src/fields/UIntField.py +17 -0
- bluetti_bt_lib-0.0.1/src/fields/VersionField.py +13 -0
- bluetti_bt_lib-0.0.1/src/fields/__init__.py +12 -0
- bluetti_bt_lib-0.0.1/src/registers/DeviceRegister.py +49 -0
- bluetti_bt_lib-0.0.1/src/registers/ReadableRegisters.py +24 -0
- bluetti_bt_lib-0.0.1/src/registers/WriteableRegister.py +19 -0
- bluetti_bt_lib-0.0.1/src/registers/__init__.py +3 -0
- bluetti_bt_lib-0.0.1/src/scripts.py +87 -0
- bluetti_bt_lib-0.0.1/src/utils/__init__.py +0 -0
- bluetti_bt_lib-0.0.1/src/utils/device_builder.py +24 -0
- bluetti_bt_lib-0.0.1/src/utils/device_info.py +12 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Patrick762
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bluetti-bt-lib
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Bluetti BT
|
|
5
|
+
Home-page: https://github.com/Patrick762/bluetti-bt-lib
|
|
6
|
+
Author: Patrick762
|
|
7
|
+
Author-email: <pip-bluetti-bt-lib@hosting-rt.de>
|
|
8
|
+
Classifier: Development Status :: 1 - Planning
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: async_timeout
|
|
14
|
+
Requires-Dist: asyncio
|
|
15
|
+
Requires-Dist: bleak
|
|
16
|
+
Requires-Dist: crcmod
|
|
17
|
+
Requires-Dist: cryptography
|
|
18
|
+
Requires-Dist: logging
|
|
19
|
+
Requires-Dist: pyasn1
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# bluetti-bt-lib
|
|
32
|
+
Inofficial Library for basic communication to bluetti powerstations.
|
|
33
|
+
|
|
34
|
+
## Disclaimer
|
|
35
|
+
This library is provided without any warranty or support by Bluetti. I do not take responsibility for any problems it may cause in all cases. Use it at your own risk.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/Patrick762/bluetti-bt-lib.git
|
|
41
|
+
pip install .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Run
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bluetti
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Supported Powerstations and data
|
|
51
|
+
|
|
52
|
+
|Device Name|total_battery_percent|dc_input_power|ac_input_power|dc_output_power|ac_output_power|
|
|
53
|
+
|-----------|---------------------|--------------|--------------|---------------|---------------|
|
|
54
|
+
|AC70 |✅ |✅ |✅ |✅ |✅ |
|
|
55
|
+
|AC180 |✅ |✅ |✅ |✅ |✅ |
|
|
56
|
+
|EB3A |✅ |✅ |✅ |✅ |✅ |
|
|
57
|
+
|Handsfree 1|✅ |✅ |✅ |✅ |✅ |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# bluetti-bt-lib
|
|
2
|
+
Inofficial Library for basic communication to bluetti powerstations.
|
|
3
|
+
|
|
4
|
+
## Disclaimer
|
|
5
|
+
This library is provided without any warranty or support by Bluetti. I do not take responsibility for any problems it may cause in all cases. Use it at your own risk.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/Patrick762/bluetti-bt-lib.git
|
|
11
|
+
pip install .
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Run
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bluetti
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Supported Powerstations and data
|
|
21
|
+
|
|
22
|
+
|Device Name|total_battery_percent|dc_input_power|ac_input_power|dc_output_power|ac_output_power|
|
|
23
|
+
|-----------|---------------------|--------------|--------------|---------------|---------------|
|
|
24
|
+
|AC70 |✅ |✅ |✅ |✅ |✅ |
|
|
25
|
+
|AC180 |✅ |✅ |✅ |✅ |✅ |
|
|
26
|
+
|EB3A |✅ |✅ |✅ |✅ |✅ |
|
|
27
|
+
|Handsfree 1|✅ |✅ |✅ |✅ |✅ |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bluetti-bt-lib
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Bluetti BT
|
|
5
|
+
Home-page: https://github.com/Patrick762/bluetti-bt-lib
|
|
6
|
+
Author: Patrick762
|
|
7
|
+
Author-email: <pip-bluetti-bt-lib@hosting-rt.de>
|
|
8
|
+
Classifier: Development Status :: 1 - Planning
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: async_timeout
|
|
14
|
+
Requires-Dist: asyncio
|
|
15
|
+
Requires-Dist: bleak
|
|
16
|
+
Requires-Dist: crcmod
|
|
17
|
+
Requires-Dist: cryptography
|
|
18
|
+
Requires-Dist: logging
|
|
19
|
+
Requires-Dist: pyasn1
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# bluetti-bt-lib
|
|
32
|
+
Inofficial Library for basic communication to bluetti powerstations.
|
|
33
|
+
|
|
34
|
+
## Disclaimer
|
|
35
|
+
This library is provided without any warranty or support by Bluetti. I do not take responsibility for any problems it may cause in all cases. Use it at your own risk.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/Patrick762/bluetti-bt-lib.git
|
|
41
|
+
pip install .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Run
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bluetti
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Supported Powerstations and data
|
|
51
|
+
|
|
52
|
+
|Device Name|total_battery_percent|dc_input_power|ac_input_power|dc_output_power|ac_output_power|
|
|
53
|
+
|-----------|---------------------|--------------|--------------|---------------|---------------|
|
|
54
|
+
|AC70 |✅ |✅ |✅ |✅ |✅ |
|
|
55
|
+
|AC180 |✅ |✅ |✅ |✅ |✅ |
|
|
56
|
+
|EB3A |✅ |✅ |✅ |✅ |✅ |
|
|
57
|
+
|Handsfree 1|✅ |✅ |✅ |✅ |✅ |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
bluetti_bt_lib.egg-info/PKG-INFO
|
|
5
|
+
bluetti_bt_lib.egg-info/SOURCES.txt
|
|
6
|
+
bluetti_bt_lib.egg-info/dependency_links.txt
|
|
7
|
+
bluetti_bt_lib.egg-info/entry_points.txt
|
|
8
|
+
bluetti_bt_lib.egg-info/requires.txt
|
|
9
|
+
bluetti_bt_lib.egg-info/top_level.txt
|
|
10
|
+
src/__init__.py
|
|
11
|
+
src/const.py
|
|
12
|
+
src/scripts.py
|
|
13
|
+
src/base_devices/BaseDeviceV1.py
|
|
14
|
+
src/base_devices/BaseDeviceV2.py
|
|
15
|
+
src/base_devices/BluettiDevice.py
|
|
16
|
+
src/base_devices/__init__.py
|
|
17
|
+
src/bluetooth/__init__.py
|
|
18
|
+
src/bluetooth/device_reader.py
|
|
19
|
+
src/bluetooth/device_recognizer.py
|
|
20
|
+
src/bluetooth/encryption.py
|
|
21
|
+
src/devices/__init__.py
|
|
22
|
+
src/devices/ac180.py
|
|
23
|
+
src/devices/ac70.py
|
|
24
|
+
src/devices/eb3a.py
|
|
25
|
+
src/devices/handsfree1.py
|
|
26
|
+
src/fields/BoolField.py
|
|
27
|
+
src/fields/DecimalArrayField.py
|
|
28
|
+
src/fields/DecimalField.py
|
|
29
|
+
src/fields/DeviceField.py
|
|
30
|
+
src/fields/EnumField.py
|
|
31
|
+
src/fields/FieldName.py
|
|
32
|
+
src/fields/IntField.py
|
|
33
|
+
src/fields/StringField.py
|
|
34
|
+
src/fields/SwapStringField.py
|
|
35
|
+
src/fields/UIntField.py
|
|
36
|
+
src/fields/VersionField.py
|
|
37
|
+
src/fields/__init__.py
|
|
38
|
+
src/registers/DeviceRegister.py
|
|
39
|
+
src/registers/ReadableRegisters.py
|
|
40
|
+
src/registers/WriteableRegister.py
|
|
41
|
+
src/registers/__init__.py
|
|
42
|
+
src/utils/__init__.py
|
|
43
|
+
src/utils/device_builder.py
|
|
44
|
+
src/utils/device_info.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
src
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Setup for pypi package"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import codecs
|
|
5
|
+
from setuptools import setup, find_packages
|
|
6
|
+
|
|
7
|
+
here = os.path.abspath(os.path.dirname(__file__))
|
|
8
|
+
|
|
9
|
+
with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh:
|
|
10
|
+
long_description = "\n" + fh.read()
|
|
11
|
+
|
|
12
|
+
VERSION = "0.0.1"
|
|
13
|
+
DESCRIPTION = "Bluetti BT"
|
|
14
|
+
|
|
15
|
+
# Setting up
|
|
16
|
+
setup(
|
|
17
|
+
name="bluetti-bt-lib",
|
|
18
|
+
version=VERSION,
|
|
19
|
+
author="Patrick762",
|
|
20
|
+
author_email="<pip-bluetti-bt-lib@hosting-rt.de>",
|
|
21
|
+
description=DESCRIPTION,
|
|
22
|
+
long_description_content_type="text/markdown",
|
|
23
|
+
long_description=long_description,
|
|
24
|
+
url="https://github.com/Patrick762/bluetti-bt-lib",
|
|
25
|
+
packages=find_packages(),
|
|
26
|
+
install_requires=[
|
|
27
|
+
"async_timeout",
|
|
28
|
+
"asyncio",
|
|
29
|
+
"bleak",
|
|
30
|
+
"crcmod",
|
|
31
|
+
"cryptography",
|
|
32
|
+
"logging",
|
|
33
|
+
"pyasn1",
|
|
34
|
+
],
|
|
35
|
+
keywords=[],
|
|
36
|
+
entry_points={
|
|
37
|
+
"console_scripts": ["bluetti = src.scripts:start"],
|
|
38
|
+
},
|
|
39
|
+
classifiers=[
|
|
40
|
+
"Development Status :: 1 - Planning",
|
|
41
|
+
"Intended Audience :: Developers",
|
|
42
|
+
"Programming Language :: Python :: 3",
|
|
43
|
+
],
|
|
44
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from . import BluettiDevice
|
|
4
|
+
from ..fields import DeviceField
|
|
5
|
+
from ..fields import FieldName, StringField, UIntField
|
|
6
|
+
from ..registers import ReadableRegisters
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseDeviceV1(BluettiDevice):
|
|
10
|
+
def __init__(self, additional_fields: List[DeviceField] = []):
|
|
11
|
+
super().__init__(
|
|
12
|
+
[
|
|
13
|
+
StringField(FieldName.DEVICE_TYPE, 10, 6),
|
|
14
|
+
UIntField(FieldName.BATTERY_SOC, 43),
|
|
15
|
+
UIntField(FieldName.DC_INPUT_POWER, 36),
|
|
16
|
+
UIntField(FieldName.AC_INPUT_POWER, 37),
|
|
17
|
+
UIntField(FieldName.DC_OUTPUT_POWER, 38),
|
|
18
|
+
UIntField(FieldName.AC_OUTPUT_POWER, 39),
|
|
19
|
+
]
|
|
20
|
+
+ additional_fields,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def get_device_type_registers(self) -> List[ReadableRegisters]:
|
|
24
|
+
return [
|
|
25
|
+
ReadableRegisters(10, 6),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def get_iot_version(self) -> int:
|
|
29
|
+
return 1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from . import BluettiDevice
|
|
4
|
+
from ..fields import DeviceField
|
|
5
|
+
from ..fields import FieldName, SwapStringField, UIntField
|
|
6
|
+
from ..registers import ReadableRegisters
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseDeviceV2(BluettiDevice):
|
|
10
|
+
def __init__(self, additional_fields: List[DeviceField] = []):
|
|
11
|
+
super().__init__(
|
|
12
|
+
[
|
|
13
|
+
SwapStringField(FieldName.DEVICE_TYPE, 110, 6),
|
|
14
|
+
UIntField(FieldName.BATTERY_SOC, 102),
|
|
15
|
+
]
|
|
16
|
+
+ additional_fields,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def get_device_type_registers(self) -> List[ReadableRegisters]:
|
|
20
|
+
return [
|
|
21
|
+
ReadableRegisters(110, 6),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def get_iot_version(self) -> int:
|
|
25
|
+
return 2
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from ..registers import ReadableRegisters, DeviceRegister
|
|
4
|
+
from ..fields import DeviceField
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BluettiDevice:
|
|
8
|
+
def __init__(self, fields: List[DeviceField]):
|
|
9
|
+
self.fields = fields
|
|
10
|
+
|
|
11
|
+
def addFields(self, fields: List[DeviceField]):
|
|
12
|
+
for f in fields:
|
|
13
|
+
self.fields.append(f)
|
|
14
|
+
|
|
15
|
+
def get_polling_registers(self) -> List[ReadableRegisters]:
|
|
16
|
+
result = []
|
|
17
|
+
|
|
18
|
+
# TODO Optimize this to reduce amount of registers to separately request
|
|
19
|
+
|
|
20
|
+
for f in self.fields:
|
|
21
|
+
result.append(ReadableRegisters(f.address, f.size))
|
|
22
|
+
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
def get_device_type_registers(self) -> List[ReadableRegisters]:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
def get_iot_version(self) -> int:
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def parse(self, starting_address: int, data: bytes) -> dict:
|
|
32
|
+
# Offsets and size are counted in 2 byte chunks, so for the range we
|
|
33
|
+
# need to divide the byte size by 2
|
|
34
|
+
data_size = int(len(data) / 2)
|
|
35
|
+
|
|
36
|
+
# Filter out fields not in range
|
|
37
|
+
r = range(starting_address, starting_address + data_size)
|
|
38
|
+
fields = [
|
|
39
|
+
f for f in self.fields if f.address in r and f.address + f.size - 1 in r
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Parse fields
|
|
43
|
+
parsed = {}
|
|
44
|
+
for f in fields:
|
|
45
|
+
data_start = 2 * (f.address - starting_address)
|
|
46
|
+
field_data = data[data_start : data_start + 2 * f.size]
|
|
47
|
+
parsed[f.name] = f.parse(field_data)
|
|
48
|
+
|
|
49
|
+
return parsed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .device_reader import *
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import async_timeout
|
|
4
|
+
from typing import Any, Callable, List, cast
|
|
5
|
+
from bleak import BleakClient
|
|
6
|
+
from bleak.exc import BleakError
|
|
7
|
+
|
|
8
|
+
from .encryption import BluettiEncryption, Message, MessageType
|
|
9
|
+
from ..registers import ReadableRegisters
|
|
10
|
+
from ..const import NOTIFY_UUID, WRITE_UUID
|
|
11
|
+
from ..base_devices import BluettiDevice
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeviceReaderConfig:
|
|
17
|
+
def __init__(self, timeout: int = 60, use_encryption: bool = False):
|
|
18
|
+
self.timeout = timeout
|
|
19
|
+
self.use_encryption = use_encryption
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeviceReader:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
bleak_client: BleakClient,
|
|
26
|
+
bluetti_device: BluettiDevice,
|
|
27
|
+
future_builder_method: Callable[[], asyncio.Future[Any]],
|
|
28
|
+
config: DeviceReaderConfig | None = None,
|
|
29
|
+
):
|
|
30
|
+
self.client = bleak_client
|
|
31
|
+
self.bluetti_device = bluetti_device
|
|
32
|
+
self.create_future = future_builder_method
|
|
33
|
+
|
|
34
|
+
if config is not None:
|
|
35
|
+
self.config = config
|
|
36
|
+
else:
|
|
37
|
+
self.config = DeviceReaderConfig()
|
|
38
|
+
|
|
39
|
+
self.has_notifier = False
|
|
40
|
+
self.current_registers = None
|
|
41
|
+
self.notify_response = bytearray()
|
|
42
|
+
self.notify_future: asyncio.Future[Any] | None = None
|
|
43
|
+
self.encryption = BluettiEncryption()
|
|
44
|
+
self.polling_lock = asyncio.Lock()
|
|
45
|
+
|
|
46
|
+
async def read(
|
|
47
|
+
self, only_registers: List[ReadableRegisters] | None = None
|
|
48
|
+
) -> dict | None:
|
|
49
|
+
registers = self.bluetti_device.get_polling_registers()
|
|
50
|
+
|
|
51
|
+
if only_registers is not None:
|
|
52
|
+
registers = only_registers
|
|
53
|
+
|
|
54
|
+
parsed_data: dict = {}
|
|
55
|
+
|
|
56
|
+
_LOGGER.debug("Reading device registers")
|
|
57
|
+
|
|
58
|
+
async with self.polling_lock:
|
|
59
|
+
try:
|
|
60
|
+
async with async_timeout.timeout(self.config.timeout):
|
|
61
|
+
if not self.client.is_connected:
|
|
62
|
+
_LOGGER.debug("Connecting to device")
|
|
63
|
+
await self.client.connect()
|
|
64
|
+
|
|
65
|
+
_LOGGER.debug("Connected to device")
|
|
66
|
+
|
|
67
|
+
if not self.has_notifier:
|
|
68
|
+
await self.client.start_notify(
|
|
69
|
+
NOTIFY_UUID, self._notification_handler
|
|
70
|
+
)
|
|
71
|
+
self.has_notifier = True
|
|
72
|
+
|
|
73
|
+
_LOGGER.debug("Notification handler setup complete")
|
|
74
|
+
|
|
75
|
+
while (
|
|
76
|
+
self.config.use_encryption
|
|
77
|
+
and not self.encryption.is_ready_for_commands
|
|
78
|
+
):
|
|
79
|
+
await asyncio.sleep(5)
|
|
80
|
+
_LOGGER.debug("Encryption handshake not finished yet")
|
|
81
|
+
|
|
82
|
+
for register in registers:
|
|
83
|
+
body = register.parse_response(
|
|
84
|
+
await self._async_send_command(register)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
_LOGGER.debug("Raw data: %s", body)
|
|
88
|
+
|
|
89
|
+
parsed = self.bluetti_device.parse(
|
|
90
|
+
register.starting_address, body
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
_LOGGER.debug("Parsed data: %s", parsed)
|
|
94
|
+
|
|
95
|
+
parsed_data.update(parsed)
|
|
96
|
+
|
|
97
|
+
except TimeoutError:
|
|
98
|
+
_LOGGER.warning("Timeout")
|
|
99
|
+
return None
|
|
100
|
+
except BleakError as err:
|
|
101
|
+
_LOGGER.warning("Bleak error: %s", err)
|
|
102
|
+
return None
|
|
103
|
+
except BaseException as err:
|
|
104
|
+
_LOGGER.warning("Unknown error: %s", err)
|
|
105
|
+
return None
|
|
106
|
+
finally:
|
|
107
|
+
if self.has_notifier:
|
|
108
|
+
try:
|
|
109
|
+
await self.client.stop_notify(NOTIFY_UUID)
|
|
110
|
+
_LOGGER.debug("Stopped notifier")
|
|
111
|
+
except:
|
|
112
|
+
# Ignore errors here
|
|
113
|
+
pass
|
|
114
|
+
self.has_notifier = False
|
|
115
|
+
await self.client.disconnect()
|
|
116
|
+
_LOGGER.debug("Disconnected from device")
|
|
117
|
+
|
|
118
|
+
# Reset Encryption keys
|
|
119
|
+
self.encryption.reset()
|
|
120
|
+
|
|
121
|
+
# Check if dict is empty
|
|
122
|
+
if not parsed_data:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
return parsed_data
|
|
126
|
+
|
|
127
|
+
async def _async_send_command(self, registers: ReadableRegisters) -> bytes:
|
|
128
|
+
"""Send command and return response"""
|
|
129
|
+
self.current_registers = registers
|
|
130
|
+
self.notify_response = bytearray()
|
|
131
|
+
self.notify_future = self.create_future()
|
|
132
|
+
|
|
133
|
+
command_bytes = bytes(registers)
|
|
134
|
+
|
|
135
|
+
# Encrypt command
|
|
136
|
+
if self.config.use_encryption is True:
|
|
137
|
+
if not self.encryption.is_ready_for_commands:
|
|
138
|
+
return bytes()
|
|
139
|
+
command_bytes = self.encryption.aes_encrypt(
|
|
140
|
+
command_bytes, self.encryption.secure_aes_key, None
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Make request
|
|
145
|
+
await self.client.write_gatt_char(WRITE_UUID, command_bytes)
|
|
146
|
+
|
|
147
|
+
_LOGGER.debug("Request sent (%s)", registers)
|
|
148
|
+
|
|
149
|
+
# Wait for response
|
|
150
|
+
res = await asyncio.wait_for(self.notify_future, timeout=5)
|
|
151
|
+
|
|
152
|
+
_LOGGER.debug("Got response")
|
|
153
|
+
|
|
154
|
+
return cast(bytes, res)
|
|
155
|
+
except:
|
|
156
|
+
_LOGGER.warning("Error while reading data")
|
|
157
|
+
|
|
158
|
+
return bytes()
|
|
159
|
+
|
|
160
|
+
async def _notification_handler(self, _: int, data: bytearray):
|
|
161
|
+
"""Handle bt data."""
|
|
162
|
+
_LOGGER.debug("Got new data")
|
|
163
|
+
|
|
164
|
+
if self.config.use_encryption is True:
|
|
165
|
+
message = Message(data)
|
|
166
|
+
|
|
167
|
+
if message.is_pre_key_exchange:
|
|
168
|
+
message.verify_checksum()
|
|
169
|
+
|
|
170
|
+
if message.type == MessageType.CHALLENGE:
|
|
171
|
+
challenge_response = self.encryption.msg_challenge(message)
|
|
172
|
+
await self.client.write_gatt_char(WRITE_UUID, challenge_response)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if message.type == MessageType.CHALLENGE_ACCEPTED:
|
|
176
|
+
_LOGGER.debug("Challenge accepted")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if self.encryption.unsecure_aes_key is None:
|
|
180
|
+
_LOGGER.error("Received encrypted message before key initialization")
|
|
181
|
+
|
|
182
|
+
key, iv = self.encryption.getKeyIv()
|
|
183
|
+
decrypted = Message(self.encryption.aes_decrypt(message.buffer, key, iv))
|
|
184
|
+
|
|
185
|
+
if decrypted.is_pre_key_exchange:
|
|
186
|
+
decrypted.verify_checksum()
|
|
187
|
+
|
|
188
|
+
if decrypted.type == MessageType.PEER_PUBKEY:
|
|
189
|
+
peer_pubkey_response = self.encryption.msg_peer_pubkey(decrypted)
|
|
190
|
+
await self.client.write_gatt_char(WRITE_UUID, peer_pubkey_response)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if decrypted.type == MessageType.PUBKEY_ACCEPTED:
|
|
194
|
+
self.encryption.msg_key_accepted(decrypted)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Handle as message
|
|
198
|
+
data = decrypted.buffer
|
|
199
|
+
|
|
200
|
+
# Save data
|
|
201
|
+
self.notify_response.extend(data)
|
|
202
|
+
self.notify_future.set_result(self.notify_response)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Callable, List
|
|
4
|
+
from bleak import BleakClient
|
|
5
|
+
|
|
6
|
+
from ..base_devices import BluettiDevice, BaseDeviceV1, BaseDeviceV2
|
|
7
|
+
from ..bluetooth import DeviceReader, DeviceReaderConfig
|
|
8
|
+
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DeviceRecognizerResult:
|
|
13
|
+
def __init__(self, name: str, iot_version: int, encrypted: bool):
|
|
14
|
+
self.name = name
|
|
15
|
+
self.iot_version = iot_version
|
|
16
|
+
self.encrypted = encrypted
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def recognize_device(
|
|
20
|
+
bleak_client: BleakClient,
|
|
21
|
+
future_builder_method: Callable[[], asyncio.Future[Any]],
|
|
22
|
+
) -> DeviceRecognizerResult | None:
|
|
23
|
+
# Since we don't know the type we use the base device
|
|
24
|
+
bluetti_devices: List[BluettiDevice] = [
|
|
25
|
+
BaseDeviceV2(),
|
|
26
|
+
BaseDeviceV1(),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
for bluetti_device in bluetti_devices:
|
|
30
|
+
# Create device builder
|
|
31
|
+
device_readers = [
|
|
32
|
+
DeviceReader(
|
|
33
|
+
bleak_client,
|
|
34
|
+
bluetti_device,
|
|
35
|
+
future_builder_method,
|
|
36
|
+
DeviceReaderConfig(
|
|
37
|
+
timeout=8,
|
|
38
|
+
use_encryption=True,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
DeviceReader(
|
|
42
|
+
bleak_client,
|
|
43
|
+
bluetti_device,
|
|
44
|
+
future_builder_method,
|
|
45
|
+
DeviceReaderConfig(timeout=3),
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for device_reader in device_readers:
|
|
50
|
+
|
|
51
|
+
# We only need 6 registers to get the device type
|
|
52
|
+
data = await device_reader.read(
|
|
53
|
+
bluetti_device.get_device_type_registers(),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if data is None:
|
|
57
|
+
# Should not happen
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
field_data = data.get("device_type")
|
|
61
|
+
|
|
62
|
+
if field_data is None:
|
|
63
|
+
# We have a problem
|
|
64
|
+
_LOGGER.error("No data in device type field_data")
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if not isinstance(field_data, str):
|
|
68
|
+
# We have a problem
|
|
69
|
+
_LOGGER.error("Invalid data in device type field_data")
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if field_data == "":
|
|
73
|
+
# Empty string is not a valid device type
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
return DeviceRecognizerResult(
|
|
77
|
+
field_data,
|
|
78
|
+
bluetti_device.get_iot_version(),
|
|
79
|
+
device_reader.config.use_encryption,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return None
|