SolixBLE 3.8.0__tar.gz → 3.9.0__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.
- {solixble-3.8.0 → solixble-3.9.0}/PKG-INFO +3 -1
- {solixble-3.8.0 → solixble-3.9.0}/README.md +2 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/__init__.py +4 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/device.py +62 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/__init__.py +4 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c1000.py +19 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c300dc.py +107 -1
- solixble-3.9.0/SolixBLE/devices/f2600.py +501 -0
- solixble-3.9.0/SolixBLE/devices/maggo_3in1.py +124 -0
- solixble-3.9.0/SolixBLE/devices/prime_charger_250w.py +477 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/PKG-INFO +3 -1
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/SOURCES.txt +2 -0
- {solixble-3.8.0 → solixble-3.9.0}/pyproject.toml +1 -1
- {solixble-3.8.0 → solixble-3.9.0}/tests/test_devices.py +508 -0
- solixble-3.8.0/SolixBLE/devices/prime_charger_250w.py +0 -278
- {solixble-3.8.0 → solixble-3.9.0}/LICENSE.txt +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/const.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c1000g2.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c300.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c800.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/f2000.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/f3800.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/generic.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/prime_charger_160w.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/prime_power_bank_20k.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/solarbank2.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/solarbank3.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/prime_device.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/states.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/utilities.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/dependency_links.txt +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/requires.txt +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/top_level.txt +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/setup.cfg +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/tests/test_connection.py +0 -0
- {solixble-3.8.0 → solixble-3.9.0}/tests/test_prime.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SolixBLE
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.0
|
|
4
4
|
Summary: Python module for monitoring & controlling Bluetooth Anker Solix devices
|
|
5
5
|
Author-email: Harvey Lelliott <harveylelliott@duck.com>
|
|
6
6
|
License: MIT License
|
|
@@ -97,11 +97,13 @@ See the [support table](https://solixble.readthedocs.io/en/latest) in the docume
|
|
|
97
97
|
- C1000(X)
|
|
98
98
|
- C1000 Gen 2
|
|
99
99
|
- F2000 (767 PowerHouse)
|
|
100
|
+
- F2600
|
|
100
101
|
- F3800
|
|
101
102
|
- Solarbank 2
|
|
102
103
|
- Solarbank 3
|
|
103
104
|
- Prime Charger 160w
|
|
104
105
|
- Prime Charger 250w
|
|
106
|
+
- Prime MagGo 3in1 (A25X7)
|
|
105
107
|
- Potentially more!
|
|
106
108
|
|
|
107
109
|
|
|
@@ -48,11 +48,13 @@ See the [support table](https://solixble.readthedocs.io/en/latest) in the docume
|
|
|
48
48
|
- C1000(X)
|
|
49
49
|
- C1000 Gen 2
|
|
50
50
|
- F2000 (767 PowerHouse)
|
|
51
|
+
- F2600
|
|
51
52
|
- F3800
|
|
52
53
|
- Solarbank 2
|
|
53
54
|
- Solarbank 3
|
|
54
55
|
- Prime Charger 160w
|
|
55
56
|
- Prime Charger 250w
|
|
57
|
+
- Prime MagGo 3in1 (A25X7)
|
|
56
58
|
- Potentially more!
|
|
57
59
|
|
|
58
60
|
|
|
@@ -12,8 +12,10 @@ from .devices import (
|
|
|
12
12
|
C1000,
|
|
13
13
|
C1000G2,
|
|
14
14
|
F2000,
|
|
15
|
+
F2600,
|
|
15
16
|
F3800,
|
|
16
17
|
Generic,
|
|
18
|
+
MagGo3in1,
|
|
17
19
|
PrimeCharger160w,
|
|
18
20
|
PrimeCharger250w,
|
|
19
21
|
PrimePowerBank20k,
|
|
@@ -41,12 +43,14 @@ __all__ = [
|
|
|
41
43
|
"C1000",
|
|
42
44
|
"C1000G2",
|
|
43
45
|
"F2000",
|
|
46
|
+
"F2600",
|
|
44
47
|
"F3800",
|
|
45
48
|
"Solarbank2",
|
|
46
49
|
"Solarbank3",
|
|
47
50
|
"PrimeCharger160w",
|
|
48
51
|
"PrimeCharger250w",
|
|
49
52
|
"PrimePowerBank20k",
|
|
53
|
+
"MagGo3in1",
|
|
50
54
|
"Generic",
|
|
51
55
|
"ChargingStatus",
|
|
52
56
|
"ChargingStatusF3800",
|
|
@@ -76,6 +76,7 @@ class SolixBLEDevice:
|
|
|
76
76
|
self._state_changed_callbacks: list[Callable[[], None]] = []
|
|
77
77
|
self._packet_futures: dict[bytes, list[asyncio.Future]] = {}
|
|
78
78
|
self._auto_reconnect_task: asyncio.Task | None = None
|
|
79
|
+
self._keep_alive_task: asyncio.Task | None = None
|
|
79
80
|
self._disconnect_event: asyncio.Event = asyncio.Event()
|
|
80
81
|
self._connection_attempts: int = 0
|
|
81
82
|
self._shared_secret: bytes | None = None
|
|
@@ -213,6 +214,10 @@ class SolixBLEDevice:
|
|
|
213
214
|
if self._auto_reconnect_task is None:
|
|
214
215
|
self._auto_reconnect_task = asyncio.create_task(self._auto_reconnect())
|
|
215
216
|
|
|
217
|
+
# Start a keep-alive task if its not running already
|
|
218
|
+
if self._keep_alive_task is None:
|
|
219
|
+
self._keep_alive_task = asyncio.create_task(self._keep_alive_fn())
|
|
220
|
+
|
|
216
221
|
# Execute callbacks if enabled
|
|
217
222
|
if run_callbacks:
|
|
218
223
|
self._run_state_changed_callbacks()
|
|
@@ -230,6 +235,26 @@ class SolixBLEDevice:
|
|
|
230
235
|
"""
|
|
231
236
|
pass
|
|
232
237
|
|
|
238
|
+
async def _keep_alive(self) -> int | None:
|
|
239
|
+
"""Execute designated keep-alive command periodically after good negotiation.
|
|
240
|
+
|
|
241
|
+
Use this to execute code periodically that is needed to keep the
|
|
242
|
+
connection. For example some devices need a special keep alive command
|
|
243
|
+
to keep receiving telemetry updates that must be sent every ~10 seconds.
|
|
244
|
+
|
|
245
|
+
This is automatically executed in a task created by :meth:`connect` once
|
|
246
|
+
the encrypted session has been negotiated (so :meth:`_send_command`
|
|
247
|
+
may be used) and on every automatic reconnect, with it not being
|
|
248
|
+
executed when not connected or negotiated.
|
|
249
|
+
|
|
250
|
+
The default implementation does nothing; subclasses can override it to,
|
|
251
|
+
for example, send a subscribe command to keep a telemetry stream (see
|
|
252
|
+
:class:`~SolixBLE.devices.prime_charger_250w.PrimeCharger250w`).
|
|
253
|
+
|
|
254
|
+
:returns: Seconds to wait before calling again or None for not implemented.
|
|
255
|
+
"""
|
|
256
|
+
return None
|
|
257
|
+
|
|
233
258
|
async def disconnect(self) -> None:
|
|
234
259
|
"""Disconnect from device and reset internal state.
|
|
235
260
|
|
|
@@ -242,6 +267,10 @@ class SolixBLEDevice:
|
|
|
242
267
|
if self._auto_reconnect_task is not None:
|
|
243
268
|
self._auto_reconnect_task.cancel()
|
|
244
269
|
|
|
270
|
+
# Cancel the keep-alive task
|
|
271
|
+
if self._keep_alive_task is not None:
|
|
272
|
+
self._keep_alive_task.cancel()
|
|
273
|
+
|
|
245
274
|
# If there is a client disconnect and throw it away
|
|
246
275
|
if self._client is not None:
|
|
247
276
|
await self._dispose_of_client()
|
|
@@ -1025,6 +1054,39 @@ class SolixBLEDevice:
|
|
|
1025
1054
|
except Exception:
|
|
1026
1055
|
_LOGGER.exception("Unexpected exception in automatic reconnect task!")
|
|
1027
1056
|
|
|
1057
|
+
|
|
1058
|
+
async def _keep_alive_fn(self) -> None:
|
|
1059
|
+
"""Task designed to be run in background to execute keep-alive.
|
|
1060
|
+
|
|
1061
|
+
This task is executed automatically when a successful connection
|
|
1062
|
+
is made and will periodically execute the keep alive function if
|
|
1063
|
+
one exists.
|
|
1064
|
+
|
|
1065
|
+
This background task is cancelled when the connection is lost.
|
|
1066
|
+
"""
|
|
1067
|
+
try:
|
|
1068
|
+
while self.negotiated:
|
|
1069
|
+
|
|
1070
|
+
try:
|
|
1071
|
+
_LOGGER.debug("Executing keep-alive...")
|
|
1072
|
+
result = await self._keep_alive()
|
|
1073
|
+
|
|
1074
|
+
if result is None:
|
|
1075
|
+
_LOGGER.debug("No keep-alive task registered, stopping task...")
|
|
1076
|
+
return
|
|
1077
|
+
|
|
1078
|
+
_LOGGER.debug(f"Executing keep-alive in {result}s")
|
|
1079
|
+
await asyncio.sleep(result)
|
|
1080
|
+
|
|
1081
|
+
except Exception:
|
|
1082
|
+
_LOGGER.exception("Exception raised executing keep-alive function!")
|
|
1083
|
+
|
|
1084
|
+
except asyncio.CancelledError:
|
|
1085
|
+
_LOGGER.debug("Keep-alive task has been canceled/stopped")
|
|
1086
|
+
|
|
1087
|
+
except Exception:
|
|
1088
|
+
_LOGGER.exception("Unexpected exception in keep-alive task!")
|
|
1089
|
+
|
|
1028
1090
|
def _disconnect_callback(self, client: BaseBleakClient) -> None:
|
|
1029
1091
|
"""Callback executed by bleak when the connection is lost.
|
|
1030
1092
|
|
|
@@ -10,8 +10,10 @@ from .c800 import C800
|
|
|
10
10
|
from .c1000 import C1000
|
|
11
11
|
from .c1000g2 import C1000G2
|
|
12
12
|
from .f2000 import F2000
|
|
13
|
+
from .f2600 import F2600
|
|
13
14
|
from .f3800 import F3800
|
|
14
15
|
from .generic import Generic
|
|
16
|
+
from .maggo_3in1 import MagGo3in1
|
|
15
17
|
from .prime_charger_160w import PrimeCharger160w
|
|
16
18
|
from .prime_charger_250w import PrimeCharger250w
|
|
17
19
|
from .prime_power_bank_20k import PrimePowerBank20k
|
|
@@ -25,11 +27,13 @@ __all__ = [
|
|
|
25
27
|
"C1000",
|
|
26
28
|
"C1000G2",
|
|
27
29
|
"F2000",
|
|
30
|
+
"F2600",
|
|
28
31
|
"F3800",
|
|
29
32
|
"Solarbank2",
|
|
30
33
|
"Solarbank3",
|
|
31
34
|
"PrimeCharger160w",
|
|
32
35
|
"PrimeCharger250w",
|
|
33
36
|
"PrimePowerBank20k",
|
|
37
|
+
"MagGo3in1",
|
|
34
38
|
"Generic",
|
|
35
39
|
]
|
|
@@ -186,6 +186,25 @@ class C1000(SolixBLEDevice):
|
|
|
186
186
|
"""
|
|
187
187
|
return self._parse_int("b0", begin=1)
|
|
188
188
|
|
|
189
|
+
@property
|
|
190
|
+
def dc_output(self) -> PortStatus:
|
|
191
|
+
"""DC Port Status.
|
|
192
|
+
|
|
193
|
+
PortStatus.NOT_CONNECTED signifies off.
|
|
194
|
+
PortStatus.OUTPUT signifies on.
|
|
195
|
+
|
|
196
|
+
:returns: Status of the DC output port.
|
|
197
|
+
"""
|
|
198
|
+
return PortStatus(self._parse_int("b2", begin=1, end=2))
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def dc_power_out(self) -> int:
|
|
202
|
+
"""DC Power Out.
|
|
203
|
+
|
|
204
|
+
:returns: DC power out or default int value.
|
|
205
|
+
"""
|
|
206
|
+
return self._parse_int("b2", begin=2)
|
|
207
|
+
|
|
189
208
|
@property
|
|
190
209
|
def software_version(self) -> str:
|
|
191
210
|
"""Main software version.
|
|
@@ -13,8 +13,25 @@ from ..const import (
|
|
|
13
13
|
DEFAULT_METADATA_STRING,
|
|
14
14
|
)
|
|
15
15
|
from ..device import SolixBLEDevice
|
|
16
|
-
from ..states import
|
|
16
|
+
from ..states import (
|
|
17
|
+
ChargingStatus,
|
|
18
|
+
DisplayTimeout,
|
|
19
|
+
LightStatus,
|
|
20
|
+
PortOverload,
|
|
21
|
+
PortStatus,
|
|
22
|
+
TemperatureUnit,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
CMD_DC_OUTPUT = "404b"
|
|
26
|
+
CMD_DISPLAY_ON_OFF = "4052"
|
|
27
|
+
CMD_LIGHT_MODE = "404f"
|
|
28
|
+
CMD_DISPLAY_TIMEOUT = "4046"
|
|
29
|
+
CMD_DISPLAY_MODE = "404c"
|
|
17
30
|
|
|
31
|
+
PAYLOAD_ON = "a10121a2020101"
|
|
32
|
+
PAYLOAD_OFF = "a10121a2020100"
|
|
33
|
+
PAYLOAD_LIGHT_MODE = "a10121a20201"
|
|
34
|
+
PAYLOAD_TIMEOUT_TIME = "a10121a20302"
|
|
18
35
|
|
|
19
36
|
class C300DC(SolixBLEDevice):
|
|
20
37
|
"""
|
|
@@ -381,3 +398,92 @@ class C300DC(SolixBLEDevice):
|
|
|
381
398
|
if self._data is not None
|
|
382
399
|
else DEFAULT_METADATA_BOOL
|
|
383
400
|
)
|
|
401
|
+
|
|
402
|
+
async def turn_dc_on(self) -> None:
|
|
403
|
+
"""Turn the DC output on.
|
|
404
|
+
|
|
405
|
+
:raises ConnectionError: If not connected to device.
|
|
406
|
+
:raises BleakError: If command transmission fails.
|
|
407
|
+
"""
|
|
408
|
+
await self._send_command(
|
|
409
|
+
cmd=bytes.fromhex(CMD_DC_OUTPUT), payload=bytes.fromhex(PAYLOAD_ON)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
async def turn_dc_off(self) -> None:
|
|
413
|
+
"""Turn the DC output off.
|
|
414
|
+
|
|
415
|
+
:raises ConnectionError: If not connected to device.
|
|
416
|
+
:raises BleakError: If command transmission fails.
|
|
417
|
+
"""
|
|
418
|
+
await self._send_command(
|
|
419
|
+
cmd=bytes.fromhex(CMD_DC_OUTPUT), payload=bytes.fromhex(PAYLOAD_OFF)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
async def turn_display_on(self) -> None:
|
|
423
|
+
"""Turn the display on.
|
|
424
|
+
|
|
425
|
+
:raises ConnectionError: If not connected to device.
|
|
426
|
+
:raises BleakError: If command transmission fails.
|
|
427
|
+
"""
|
|
428
|
+
await self._send_command(
|
|
429
|
+
cmd=bytes.fromhex(CMD_DISPLAY_ON_OFF), payload=bytes.fromhex(PAYLOAD_ON)
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
async def turn_display_off(self) -> None:
|
|
433
|
+
"""Turn the display off.
|
|
434
|
+
|
|
435
|
+
:raises ConnectionError: If not connected to device.
|
|
436
|
+
:raises BleakError: If command transmission fails.
|
|
437
|
+
"""
|
|
438
|
+
await self._send_command(
|
|
439
|
+
cmd=bytes.fromhex(CMD_DISPLAY_ON_OFF), payload=bytes.fromhex(PAYLOAD_OFF)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
async def set_light_mode(self, mode: LightStatus) -> None:
|
|
443
|
+
"""Set the light mode of the LED bar.
|
|
444
|
+
|
|
445
|
+
:param mode: Mode to set light bar to.
|
|
446
|
+
:raises ValueError: If requested mode is invalid.
|
|
447
|
+
:raises ConnectionError: If not connected to device.
|
|
448
|
+
:raises BleakError: If command transmission fails.
|
|
449
|
+
"""
|
|
450
|
+
if mode is LightStatus.UNKNOWN:
|
|
451
|
+
raise ValueError("You cannot set the light status to unknown")
|
|
452
|
+
await self._send_command(
|
|
453
|
+
cmd=bytes.fromhex(CMD_LIGHT_MODE),
|
|
454
|
+
payload=bytes.fromhex(PAYLOAD_LIGHT_MODE) + mode.value.to_bytes(),
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
async def set_display_timeout(self, timeout: DisplayTimeout) -> None:
|
|
458
|
+
"""Set the status/mode of the LCD display.
|
|
459
|
+
|
|
460
|
+
:param mode: Mode/timeout to set display to (30s, 5m, 30m, etc).
|
|
461
|
+
:raises ValueError: If requested mode is invalid.
|
|
462
|
+
:raises ConnectionError: If not connected to device.
|
|
463
|
+
:raises BleakError: If command transmission fails.
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
if timeout is DisplayTimeout.UNKNOWN:
|
|
467
|
+
raise ValueError("You cannot set the display timeout to unknown")
|
|
468
|
+
await self._send_command(
|
|
469
|
+
cmd=bytes.fromhex(CMD_DISPLAY_TIMEOUT),
|
|
470
|
+
payload=bytes.fromhex(PAYLOAD_TIMEOUT_TIME)
|
|
471
|
+
+ timeout.value.to_bytes(length=2, byteorder="little", signed=False),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
async def set_display_mode(self, mode: LightStatus) -> None:
|
|
475
|
+
"""Set the status/mode of the LCD display.
|
|
476
|
+
|
|
477
|
+
:param mode: Mode/status to set display to (off/low/med/high).
|
|
478
|
+
:raises ValueError: If requested mode is invalid.
|
|
479
|
+
:raises ConnectionError: If not connected to device.
|
|
480
|
+
:raises BleakError: If command transmission fails.
|
|
481
|
+
"""
|
|
482
|
+
if mode is LightStatus.UNKNOWN:
|
|
483
|
+
raise ValueError("You cannot set the display brightness status to unknown")
|
|
484
|
+
if mode is LightStatus.SOS:
|
|
485
|
+
raise ValueError("You cannot set the display brightness status to SOS")
|
|
486
|
+
await self._send_command(
|
|
487
|
+
cmd=bytes.fromhex(CMD_DISPLAY_MODE),
|
|
488
|
+
payload=bytes.fromhex(PAYLOAD_LIGHT_MODE) + mode.value.to_bytes(),
|
|
489
|
+
)
|