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.
Files changed (36) hide show
  1. {solixble-3.8.0 → solixble-3.9.0}/PKG-INFO +3 -1
  2. {solixble-3.8.0 → solixble-3.9.0}/README.md +2 -0
  3. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/__init__.py +4 -0
  4. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/device.py +62 -0
  5. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/__init__.py +4 -0
  6. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c1000.py +19 -0
  7. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c300dc.py +107 -1
  8. solixble-3.9.0/SolixBLE/devices/f2600.py +501 -0
  9. solixble-3.9.0/SolixBLE/devices/maggo_3in1.py +124 -0
  10. solixble-3.9.0/SolixBLE/devices/prime_charger_250w.py +477 -0
  11. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/PKG-INFO +3 -1
  12. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/SOURCES.txt +2 -0
  13. {solixble-3.8.0 → solixble-3.9.0}/pyproject.toml +1 -1
  14. {solixble-3.8.0 → solixble-3.9.0}/tests/test_devices.py +508 -0
  15. solixble-3.8.0/SolixBLE/devices/prime_charger_250w.py +0 -278
  16. {solixble-3.8.0 → solixble-3.9.0}/LICENSE.txt +0 -0
  17. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/const.py +0 -0
  18. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c1000g2.py +0 -0
  19. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c300.py +0 -0
  20. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/c800.py +0 -0
  21. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/f2000.py +0 -0
  22. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/f3800.py +0 -0
  23. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/generic.py +0 -0
  24. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/prime_charger_160w.py +0 -0
  25. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/prime_power_bank_20k.py +0 -0
  26. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/solarbank2.py +0 -0
  27. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/devices/solarbank3.py +0 -0
  28. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/prime_device.py +0 -0
  29. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/states.py +0 -0
  30. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE/utilities.py +0 -0
  31. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/dependency_links.txt +0 -0
  32. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/requires.txt +0 -0
  33. {solixble-3.8.0 → solixble-3.9.0}/SolixBLE.egg-info/top_level.txt +0 -0
  34. {solixble-3.8.0 → solixble-3.9.0}/setup.cfg +0 -0
  35. {solixble-3.8.0 → solixble-3.9.0}/tests/test_connection.py +0 -0
  36. {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.8.0
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 ChargingStatus, LightStatus, PortStatus, TemperatureUnit, PortOverload
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
+ )