SolixBLE 3.5.0__tar.gz → 3.6.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 (33) hide show
  1. {solixble-3.5.0 → solixble-3.6.0}/PKG-INFO +2 -1
  2. {solixble-3.5.0 → solixble-3.6.0}/README.md +1 -0
  3. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/__init__.py +4 -0
  4. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/const.py +1 -1
  5. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/device.py +197 -127
  6. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/__init__.py +2 -0
  7. solixble-3.6.0/SolixBLE/devices/prime_charger_160w.py +261 -0
  8. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/prime_charger_250w.py +20 -20
  9. solixble-3.6.0/SolixBLE/prime_device.py +522 -0
  10. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/utilities.py +9 -0
  11. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE.egg-info/PKG-INFO +2 -1
  12. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE.egg-info/SOURCES.txt +4 -1
  13. {solixble-3.5.0 → solixble-3.6.0}/pyproject.toml +1 -1
  14. solixble-3.6.0/tests/test_connection.py +255 -0
  15. {solixble-3.5.0 → solixble-3.6.0}/tests/test_devices.py +189 -131
  16. solixble-3.6.0/tests/test_prime.py +74 -0
  17. solixble-3.5.0/tests/test_reconnect.py +0 -155
  18. {solixble-3.5.0 → solixble-3.6.0}/LICENSE.txt +0 -0
  19. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/c1000.py +0 -0
  20. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/c1000g2.py +0 -0
  21. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/c300.py +0 -0
  22. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/c300dc.py +0 -0
  23. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/c800.py +0 -0
  24. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/f2000.py +0 -0
  25. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/f3800.py +0 -0
  26. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/generic.py +0 -0
  27. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/solarbank2.py +0 -0
  28. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/devices/solarbank3.py +0 -0
  29. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE/states.py +0 -0
  30. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE.egg-info/dependency_links.txt +0 -0
  31. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE.egg-info/requires.txt +0 -0
  32. {solixble-3.5.0 → solixble-3.6.0}/SolixBLE.egg-info/top_level.txt +0 -0
  33. {solixble-3.5.0 → solixble-3.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SolixBLE
3
- Version: 3.5.0
3
+ Version: 3.6.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
@@ -100,6 +100,7 @@ See the [support table](https://solixble.readthedocs.io/en/latest) in the docume
100
100
  - F3800
101
101
  - Solarbank 2
102
102
  - Solarbank 3
103
+ - Prime Charger 160w
103
104
  - Prime Charger 250w
104
105
  - Potentially more!
105
106
 
@@ -51,6 +51,7 @@ See the [support table](https://solixble.readthedocs.io/en/latest) in the docume
51
51
  - F3800
52
52
  - Solarbank 2
53
53
  - Solarbank 3
54
+ - Prime Charger 160w
54
55
  - Prime Charger 250w
55
56
  - Potentially more!
56
57
 
@@ -14,10 +14,12 @@ from .devices import (
14
14
  F2000,
15
15
  F3800,
16
16
  Generic,
17
+ PrimeCharger160w,
17
18
  PrimeCharger250w,
18
19
  Solarbank2,
19
20
  Solarbank3,
20
21
  )
22
+ from .prime_device import PrimeDevice
21
23
  from .states import (
22
24
  ChargingStatus,
23
25
  ChargingStatusF3800,
@@ -31,6 +33,7 @@ from .utilities import discover_devices
31
33
 
32
34
  __all__ = [
33
35
  "SolixBLEDevice",
36
+ "PrimeDevice",
34
37
  "C300",
35
38
  "C300DC",
36
39
  "C800",
@@ -40,6 +43,7 @@ __all__ = [
40
43
  "F3800",
41
44
  "Solarbank2",
42
45
  "Solarbank3",
46
+ "PrimeCharger160w",
43
47
  "PrimeCharger250w",
44
48
  "Generic",
45
49
  "ChargingStatus",
@@ -10,7 +10,7 @@ UUID_TELEMETRY = "8c850003-0302-41c5-b46e-cf057c562025"
10
10
  #: GATT Service UUID for sending commands / negotiating.
11
11
  UUID_COMMAND = "8c850002-0302-41c5-b46e-cf057c562025"
12
12
 
13
- #: GATT Service UUID for identifying Solix devices (Tested on C300X and C1000).
13
+ #: GATT Service UUID for identifying Solix/Prime devices (Tested on C300X, C1000, and Prime 160w Charger).
14
14
  UUID_IDENTIFIER = "0000ff09-0000-1000-8000-00805f9b34fb"
15
15
 
16
16
  #: Time to wait before re-connecting on an unexpected disconnect.
@@ -73,8 +73,7 @@ class SolixBLEDevice:
73
73
  self._auto_reconnect_task: asyncio.Task | None = None
74
74
  self._disconnect_event: asyncio.Event = asyncio.Event()
75
75
  self._connection_attempts: int = 0
76
- self._shared_key: bytes | None = None
77
- self._iv: bytes | None = None
76
+ self._shared_secret: bytes | None = None
78
77
 
79
78
  def add_callback(self, function: Callable[[], None]) -> None:
80
79
  """Register a callback to be run on state updates.
@@ -94,6 +93,14 @@ class SolixBLEDevice:
94
93
  """
95
94
  self._state_changed_callbacks.remove(function)
96
95
 
96
+ async def _initiate_negotiations(self) -> None:
97
+ """Send the negotiation initiation command."""
98
+ await self._client.write_gatt_char(
99
+ UUID_COMMAND,
100
+ bytes.fromhex(NEGOTIATION_COMMAND_0),
101
+ response=True,
102
+ )
103
+
97
104
  async def connect(self, max_attempts: int = 3, run_callbacks: bool = True) -> bool:
98
105
  """Connect to device.
99
106
 
@@ -108,12 +115,8 @@ class SolixBLEDevice:
108
115
  try:
109
116
 
110
117
  # If we have an old client get rid of it
111
- if self._client is not None and self._client.is_connected:
112
- _LOGGER.debug(
113
- f"Disposing of old client '{self._client}' in order to connect to '{self.name}'!"
114
- )
115
- await self._client.disconnect()
116
- self._client = None
118
+ if self._client is not None:
119
+ await self._dispose_of_client()
117
120
 
118
121
  # Reset negotiated details but keep any data
119
122
  self._reset_session(reset_data=False)
@@ -170,11 +173,7 @@ class SolixBLEDevice:
170
173
  _LOGGER.debug(
171
174
  f"Sending negotiation initiation request to '{self.name}'..."
172
175
  )
173
- await self._client.write_gatt_char(
174
- UUID_COMMAND,
175
- bytes.fromhex(NEGOTIATION_COMMAND_0),
176
- response=True,
177
- )
176
+ await self._initiate_negotiations()
178
177
 
179
178
  # Wait at this long to see if we get any response to
180
179
  # our initial request in stage 0. This weird layout
@@ -218,14 +217,14 @@ class SolixBLEDevice:
218
217
  if self._auto_reconnect_task is not None:
219
218
  self._auto_reconnect_task.cancel()
220
219
 
220
+ # If there is a client disconnect and throw it away
221
+ if self._client is not None:
222
+ await self._dispose_of_client()
223
+
224
+ # Reset session
221
225
  self._connection_attempts = 0
222
226
  self._reset_session()
223
227
 
224
- # If there is a client disconnect and throw it away
225
- if self._client:
226
- await self._client.disconnect()
227
- self._client = None
228
-
229
228
  @property
230
229
  def connected(self) -> bool:
231
230
  """Connected to device.
@@ -247,12 +246,7 @@ class SolixBLEDevice:
247
246
 
248
247
  :returns: True/False if session has been negotiated and connected.
249
248
  """
250
- return (
251
- self.connected
252
- and self._shared_key is not None
253
- and self._iv is not None
254
- and self._negotiation_timestamp is not None
255
- )
249
+ return self.connected and self._shared_secret is not None
256
250
 
257
251
  @property
258
252
  def available(self) -> bool:
@@ -363,48 +357,88 @@ class SolixBLEDevice:
363
357
 
364
358
  return packet_pattern, packet_cmd, packet_payload
365
359
 
366
- def _parse_payload(self, payload: bytearray) -> dict[str, bytes]:
367
- """Parse payload bytes into parameters."""
360
+ def _parse_payload(self, payload: bytearray | bytes) -> dict[str, bytes]:
361
+ """
362
+ Parse payload bytes into parameters.
363
+
364
+ Payloads contain a list of parameters and these parameters
365
+ have a format of: <id 1B> <len 1-2B> <type 1B> <data nB>.
366
+
367
+ If an error occurs when decoding a parameter it prevents all
368
+ further parameters from being parsed and logs an exception,
369
+ but the successfully parsed parameters (if any) will be returned.
370
+
371
+ :param payload: Payload to parse into parameters.
372
+ :returns: Dictionary mapping parameter ids (a1, a2, ...) to data.
373
+ """
374
+
375
+ def _verbose_pop(data: bytearray, length: int, name: str) -> bytes:
376
+ """
377
+ Pop specified number of bytes from bytearray and log if error.
378
+
379
+ :param data: Data to be popped.
380
+ :param length: Number of bytes to pop and return.
381
+ :param name: Name of value being popped to put in logs if error.
382
+ :raises IndexError: If popping fails.
383
+ """
384
+
385
+ # Copy of bytes to use in error message if needed
386
+ data_copy = bytes(data)
387
+
388
+ # Bytes extracted so far
389
+ new_bytes = bytes([])
390
+
391
+ try:
392
+ # Pop length bytes from data and return
393
+ for _ in range(length):
394
+ new_bytes = new_bytes + bytes([data.pop(0)])
395
+ return new_bytes
396
+
397
+ # Build error message
398
+ except IndexError as e:
399
+ message = (
400
+ f"Error extracting {name} (len={length}) from '{data_copy.hex()}'"
401
+ f" (len={len(data_copy)}) at index {len(new_bytes)}. We extracted:"
402
+ f" '{new_bytes.hex()}' but expected {length - len(data_copy)}"
403
+ f" more bytes!"
404
+ )
405
+ _LOGGER.exception(message)
406
+ raise IndexError(message) from e
368
407
 
369
408
  parsed_data: dict[str, bytes] = {}
370
409
  remaining_data = bytearray(payload)
371
410
 
372
- # Packets sometimes start with 00 and we must strip that
411
+ # Payloads sometimes start with 00 and we must strip that
373
412
  if remaining_data.startswith(bytes.fromhex("00")):
374
- remaining_data.pop(0)
413
+ _LOGGER.debug("Stripped 00 from start of payload")
414
+ _verbose_pop(remaining_data, 1, "special 00 header")
375
415
 
376
416
  while len(remaining_data) != 0:
377
417
  try:
378
418
  # Extract param id (e.g a1, a2, ...)
379
- param_id = bytes([remaining_data.pop(0)]).hex()
419
+ param_id = _verbose_pop(remaining_data, 1, "param_id").hex()
380
420
 
381
421
  # Sometimes there is just a param_id with no length or values
382
- # and then padding after that. This has been observed during
383
- # the optional stage 6 negotiation stage that only sometimes
384
- # seems to happen with the C300X (~ 1/20 chance).
385
- #
386
- # If we have reached PKCS7 padding then we have
387
- # reached the end of the payload
388
- if len(remaining_data) < 16 and remaining_data == bytearray(
389
- len(remaining_data) * len(remaining_data).to_bytes(1)
390
- ):
422
+ if len(remaining_data) == 0:
391
423
  parsed_data[param_id] = bytes()
392
424
  break
393
425
 
394
- param_len = remaining_data.pop(0)
395
- param_data = bytes([remaining_data.pop(0) for _ in range(0, param_len)])
396
- parsed_data[param_id] = param_data
426
+ # Extract encoded length of parameter
427
+ param_len = int.from_bytes(
428
+ _verbose_pop(remaining_data, 1, f"param_len (id={param_id})")
429
+ )
397
430
 
398
- # If we have reached PKCS7 padding then we have
399
- # reached the end of the payload
400
- if len(remaining_data) < 16 and remaining_data == bytearray(
401
- len(remaining_data) * len(remaining_data).to_bytes(1)
402
- ):
403
- break
431
+ # Extract data/body from parameter
432
+ param_data = _verbose_pop(
433
+ remaining_data, param_len, f"param_data (id={param_id})"
434
+ )
435
+ parsed_data[param_id] = param_data
404
436
 
405
437
  except IndexError:
406
438
  _LOGGER.exception(
407
- f"Unexpected end of packet! Data may be missing or invalid! Payload: '{payload.hex()}'"
439
+ f"Unexpected end of packet! Data may be missing or invalid!"
440
+ f" Extracted so far: '{self._parameters_to_str(parsed_data)}'."
441
+ f" Payload: '{payload.hex()}'"
408
442
  )
409
443
 
410
444
  return parsed_data
@@ -444,12 +478,74 @@ class SolixBLEDevice:
444
478
 
445
479
  def _decrypt_payload(self, payload: bytes) -> bytes:
446
480
  """Decrypt telemetry packet using negotiated shared secret and IV."""
447
- cipher = AES.new(self._shared_key, AES.MODE_CBC, iv=self._iv)
448
- return cipher.decrypt(payload)
481
+ cipher = AES.new(
482
+ self._shared_secret[:16], AES.MODE_CBC, iv=self._shared_secret[16:]
483
+ )
484
+ decrypted = cipher.decrypt(payload)
485
+ unpadder = PKCS7(128).unpadder()
486
+ unpadded_data = unpadder.update(decrypted)
487
+ return unpadded_data + unpadder.finalize()
449
488
 
450
- async def _process_telemetry(
451
- self, cmd: bytes, parameters: dict[str, bytes]
452
- ) -> None:
489
+ def _encrypt_payload(self, payload: bytes) -> bytes:
490
+ """Encrypt telemetry packet using negotiated shared secret and IV."""
491
+
492
+ # Pad and encrypt payload
493
+ padder = PKCS7(128).padder()
494
+ padded_data = padder.update(payload)
495
+ padded_data += padder.finalize()
496
+ cipher = AES.new(
497
+ self._shared_secret[:16], AES.MODE_CBC, iv=self._shared_secret[16:]
498
+ )
499
+ return cipher.encrypt(padded_data)
500
+
501
+ async def _process_telemetry_packet(self, payload: bytes) -> None:
502
+ """Process a telemetry packet from the device.
503
+
504
+ This performs the default processing of telemetry packets in which
505
+ telemetry payloads are spread across multiple packets. This is
506
+ overridden for devices which do not use multi-packet payloads for
507
+ telemetry.
508
+ """
509
+
510
+ # Anker devices seem to split data across multiple
511
+ # packets so we need to wait until we have both
512
+ # packets before we can decrypt all of the data
513
+ if len(payload) < 50:
514
+ self._telemetry_payload_small = payload
515
+
516
+ # If we receive a big packet it invalidates the
517
+ # last small one since the big one comes before
518
+ # the small one
519
+ elif len(payload) > 230:
520
+ self._telemetry_payload_large = payload
521
+ self._telemetry_payload_small = None
522
+
523
+ else:
524
+ _LOGGER.warning(
525
+ f"Telemetry payload has an unexpected length of {len(payload)}!"
526
+ )
527
+
528
+ if (
529
+ self._telemetry_payload_small is None
530
+ or self._telemetry_payload_large is None
531
+ ):
532
+ _LOGGER.debug("Missing other payload!")
533
+ return
534
+
535
+ new_payload = self._telemetry_payload_large + self._telemetry_payload_small
536
+
537
+ # If we are accepting the new payload we invalidate
538
+ # the partial payloads
539
+ self._telemetry_payload_large = None
540
+ self._telemetry_payload_small = None
541
+
542
+ _LOGGER.debug(f"Merged payload: {new_payload.hex()}")
543
+ decrypted_payload = self._decrypt_payload(new_payload)
544
+ _LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
545
+ parameters = self._parse_payload(decrypted_payload)
546
+ return await self._process_telemetry(parameters)
547
+
548
+ async def _process_telemetry(self, parameters: dict[str, bytes]) -> None:
453
549
  """Process telemetry data from the device."""
454
550
 
455
551
  state_changed = self._data is None or parameters != self._data
@@ -524,63 +620,31 @@ class SolixBLEDevice:
524
620
  return await self._process_negotiation(cmd, payload)
525
621
 
526
622
  # Encrypted messages
527
- case "03010f":
623
+ case "03010f" | "030111":
528
624
 
529
625
  match cmd.hex():
530
626
 
531
627
  # Telemetry messages
532
- case "c402":
628
+ case "c402" | "4300":
533
629
  _LOGGER.debug("Received telemetry message!")
534
-
535
- # Anker devices seem to split data across multiple
536
- # packets so we need to wait until we have both
537
- # packets before we can decrypt all of the data
538
- if len(payload) < 50:
539
- self._telemetry_payload_small = payload
540
-
541
- # If we receive a big packet it invalidates the
542
- # last small one since the big one comes before
543
- # the small one
544
- elif len(payload) > 230:
545
- self._telemetry_payload_large = payload
546
- self._telemetry_payload_small = None
547
-
548
- else:
549
- _LOGGER.warning(
550
- f"Telemetry payload has an unexpected length of {len(payload)}!"
551
- )
552
-
553
- if (
554
- self._telemetry_payload_small is None
555
- or self._telemetry_payload_large is None
556
- ):
557
- _LOGGER.debug("Missing other payload!")
558
- return
559
-
560
- new_payload = (
561
- self._telemetry_payload_large
562
- + self._telemetry_payload_small
563
- )
564
-
565
- # If we are accepting the new payload we invalidate
566
- # the partial payloads
567
- self._telemetry_payload_large = None
568
- self._telemetry_payload_small = None
569
-
570
- _LOGGER.debug(f"Merged payload: {new_payload.hex()}")
571
- decrypted_payload = self._decrypt_payload(new_payload)
572
- _LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
573
- parameters = self._parse_payload(decrypted_payload)
574
- return await self._process_telemetry(cmd, parameters)
630
+ return await self._process_telemetry_packet(payload)
575
631
 
576
632
  # Unknown messages
577
633
  case _:
578
634
  _LOGGER.debug(f"Received unknown message of type: {cmd.hex()}")
579
635
  try:
580
636
 
581
- # If the payload is one byte too short try putting the
637
+ # If the payload is one byte too short and we are
638
+ # using the default AES (CBC) then try putting the
582
639
  # last byte of the cmd in front of it
583
- if len(payload) % 16 == 15:
640
+ if (
641
+ len(payload) % 16 == 15
642
+ and self._decrypt_payload
643
+ is SolixBLEDevice._decrypt_payload
644
+ ):
645
+ _LOGGER.debug(
646
+ "Using special trick of embedded part of CMD in payload..."
647
+ )
584
648
  payload = cmd[1].to_bytes() + payload
585
649
 
586
650
  decrypted_payload = self._decrypt_payload(payload)
@@ -682,12 +746,8 @@ class SolixBLEDevice:
682
746
  bytes.fromhex(PRIVATE_KEY), byteorder="big"
683
747
  )
684
748
  private_key = derive_private_key(private_value, SECP256R1())
685
- shared_secret = private_key.exchange(ECDH(), device_public_key)
686
- self._shared_key = shared_secret[:16]
687
- self._iv = shared_secret[16:]
688
- _LOGGER.debug(f"Shared secret: {shared_secret.hex()}")
689
- _LOGGER.debug(f"AES key: {self._shared_key.hex()}")
690
- _LOGGER.debug(f"AES IV: {self._iv.hex()}")
749
+ self._shared_secret = private_key.exchange(ECDH(), device_public_key)
750
+ _LOGGER.debug(f"Shared secret: {self._shared_secret.hex()}")
691
751
 
692
752
  _LOGGER.debug("Sending stage 5 response message...")
693
753
  return await self._client.write_gatt_char(
@@ -740,34 +800,34 @@ class SolixBLEDevice:
740
800
  new_payload = payload + bytes.fromhex("fe0503") + new_timestamp
741
801
  await self._send_encrypted_packet(cmd, new_payload)
742
802
 
743
- async def _send_encrypted_packet(self, cmd: bytes, payload: bytes) -> None:
744
- """Send an encrypted packet using negotiated shared secret and IV."""
745
- _LOGGER.debug(
746
- f"Building packet with cmd: {cmd.hex()} and payload: {payload.hex()}"
747
- )
803
+ def _build_packet(self, pattern: bytes, cmd: bytes, payload: bytes) -> bytes:
804
+ """
805
+ Build a packet to be send to a device.
748
806
 
749
- # Pad payload
750
- padder = PKCS7(128).padder()
751
- padded_data = padder.update(payload)
752
- padded_data += padder.finalize()
807
+ Packet format: <HEADER 2B> <LENGTH 2B> <PATTERN 3B> <CMD 2B> <PAYLOAD bB> <CHECKSUM 1B>.
753
808
 
754
- # Encrypt payload
755
- cipher = AES.new(self._shared_key, AES.MODE_CBC, iv=self._iv)
756
- encrypted_payload = cipher.encrypt(padded_data)
809
+ :param pattern: Pattern of packet (e.g encrypted, negotiation, etc).
810
+ :param cmd: Command in packet (e.g telemetry, power on, etc).
811
+ :param payload: Payload of command (e.g a1...).
812
+ :returns: Packet bytes ready to be sent.
813
+ """
757
814
 
758
815
  # Calculate length of message
759
- length = 2 + 2 + 3 + 2 + len(encrypted_payload) + 1
816
+ length = 2 + 2 + 3 + 2 + len(payload) + 1
760
817
  length_bytes = length.to_bytes(length=2, byteorder="little")
761
818
 
762
819
  # Build packet
763
- packet = (
764
- bytes.fromhex("ff09")
765
- + length_bytes
766
- + bytes.fromhex("03000f")
767
- + cmd
768
- + encrypted_payload
820
+ packet = bytes.fromhex("ff09") + length_bytes + pattern + cmd + payload
821
+ return packet + self._checksum(packet)
822
+
823
+ async def _send_encrypted_packet(self, cmd: bytes, payload: bytes) -> None:
824
+ """Send an encrypted packet using negotiated shared secret and IV."""
825
+ _LOGGER.debug(
826
+ f"Building packet with cmd: {cmd.hex()} and payload: {payload.hex()}"
769
827
  )
770
- packet = packet + self._checksum(packet)
828
+ encrypted_payload = self._encrypt_payload(payload)
829
+
830
+ packet = self._build_packet(bytes.fromhex("03000f"), cmd, encrypted_payload)
771
831
  _LOGGER.debug(f"Sending encrypted packet: {packet.hex()}")
772
832
 
773
833
  # Send packet
@@ -963,7 +1023,18 @@ class SolixBLEDevice:
963
1023
  # Trigger disconnection event
964
1024
  self._disconnect_event.set()
965
1025
 
966
- def _reset_session(self, reset_data: bool = True):
1026
+ async def _dispose_of_client(self) -> None:
1027
+ """Dispose of current bleak client."""
1028
+ client = self._client
1029
+ self._client = None
1030
+ try:
1031
+ await client.disconnect()
1032
+ except Exception:
1033
+ _LOGGER.exception(
1034
+ f"Exception raised when disposing of bleak client '{client}'!"
1035
+ )
1036
+
1037
+ def _reset_session(self, reset_data: bool = True) -> None:
967
1038
  """Reset negotiated variables and data and futures."""
968
1039
 
969
1040
  if reset_data:
@@ -972,8 +1043,7 @@ class SolixBLEDevice:
972
1043
 
973
1044
  self._telemetry_payload_small = None
974
1045
  self._telemetry_payload_large = None
975
- self._shared_key = None
976
- self._iv = None
1046
+ self._shared_secret = None
977
1047
  self._last_packet_timestamp = None
978
1048
  self._negotiation_timestamp = None
979
1049
  self._packet_futures: dict[bytes, list[asyncio.Future]] = {}
@@ -12,6 +12,7 @@ from .c1000g2 import C1000G2
12
12
  from .f2000 import F2000
13
13
  from .f3800 import F3800
14
14
  from .generic import Generic
15
+ from .prime_charger_160w import PrimeCharger160w
15
16
  from .prime_charger_250w import PrimeCharger250w
16
17
  from .solarbank2 import Solarbank2
17
18
  from .solarbank3 import Solarbank3
@@ -26,6 +27,7 @@ __all__ = [
26
27
  "F3800",
27
28
  "Solarbank2",
28
29
  "Solarbank3",
30
+ "PrimeCharger160w",
29
31
  "PrimeCharger250w",
30
32
  "Generic",
31
33
  ]