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.
Files changed (46) hide show
  1. bluetti_bt_lib-0.0.1/LICENSE +21 -0
  2. bluetti_bt_lib-0.0.1/PKG-INFO +57 -0
  3. bluetti_bt_lib-0.0.1/README.md +27 -0
  4. bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/PKG-INFO +57 -0
  5. bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/SOURCES.txt +44 -0
  6. bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/dependency_links.txt +1 -0
  7. bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/entry_points.txt +2 -0
  8. bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/requires.txt +7 -0
  9. bluetti_bt_lib-0.0.1/bluetti_bt_lib.egg-info/top_level.txt +1 -0
  10. bluetti_bt_lib-0.0.1/setup.cfg +4 -0
  11. bluetti_bt_lib-0.0.1/setup.py +44 -0
  12. bluetti_bt_lib-0.0.1/src/__init__.py +2 -0
  13. bluetti_bt_lib-0.0.1/src/base_devices/BaseDeviceV1.py +29 -0
  14. bluetti_bt_lib-0.0.1/src/base_devices/BaseDeviceV2.py +25 -0
  15. bluetti_bt_lib-0.0.1/src/base_devices/BluettiDevice.py +49 -0
  16. bluetti_bt_lib-0.0.1/src/base_devices/__init__.py +3 -0
  17. bluetti_bt_lib-0.0.1/src/bluetooth/__init__.py +1 -0
  18. bluetti_bt_lib-0.0.1/src/bluetooth/device_reader.py +202 -0
  19. bluetti_bt_lib-0.0.1/src/bluetooth/device_recognizer.py +82 -0
  20. bluetti_bt_lib-0.0.1/src/bluetooth/encryption.py +296 -0
  21. bluetti_bt_lib-0.0.1/src/const.py +2 -0
  22. bluetti_bt_lib-0.0.1/src/devices/__init__.py +19 -0
  23. bluetti_bt_lib-0.0.1/src/devices/ac180.py +15 -0
  24. bluetti_bt_lib-0.0.1/src/devices/ac70.py +15 -0
  25. bluetti_bt_lib-0.0.1/src/devices/eb3a.py +5 -0
  26. bluetti_bt_lib-0.0.1/src/devices/handsfree1.py +15 -0
  27. bluetti_bt_lib-0.0.1/src/fields/BoolField.py +11 -0
  28. bluetti_bt_lib-0.0.1/src/fields/DecimalArrayField.py +14 -0
  29. bluetti_bt_lib-0.0.1/src/fields/DecimalField.py +15 -0
  30. bluetti_bt_lib-0.0.1/src/fields/DeviceField.py +13 -0
  31. bluetti_bt_lib-0.0.1/src/fields/EnumField.py +15 -0
  32. bluetti_bt_lib-0.0.1/src/fields/FieldName.py +12 -0
  33. bluetti_bt_lib-0.0.1/src/fields/IntField.py +11 -0
  34. bluetti_bt_lib-0.0.1/src/fields/StringField.py +9 -0
  35. bluetti_bt_lib-0.0.1/src/fields/SwapStringField.py +17 -0
  36. bluetti_bt_lib-0.0.1/src/fields/UIntField.py +17 -0
  37. bluetti_bt_lib-0.0.1/src/fields/VersionField.py +13 -0
  38. bluetti_bt_lib-0.0.1/src/fields/__init__.py +12 -0
  39. bluetti_bt_lib-0.0.1/src/registers/DeviceRegister.py +49 -0
  40. bluetti_bt_lib-0.0.1/src/registers/ReadableRegisters.py +24 -0
  41. bluetti_bt_lib-0.0.1/src/registers/WriteableRegister.py +19 -0
  42. bluetti_bt_lib-0.0.1/src/registers/__init__.py +3 -0
  43. bluetti_bt_lib-0.0.1/src/scripts.py +87 -0
  44. bluetti_bt_lib-0.0.1/src/utils/__init__.py +0 -0
  45. bluetti_bt_lib-0.0.1/src/utils/device_builder.py +24 -0
  46. 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,2 @@
1
+ [console_scripts]
2
+ bluetti = src.scripts:start
@@ -0,0 +1,7 @@
1
+ async_timeout
2
+ asyncio
3
+ bleak
4
+ crcmod
5
+ cryptography
6
+ logging
7
+ pyasn1
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ from .const import *
2
+ from .scripts import *
@@ -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,3 @@
1
+ from .BluettiDevice import *
2
+ from .BaseDeviceV1 import *
3
+ from .BaseDeviceV2 import *
@@ -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