SolixBLE 3.6.0__tar.gz → 3.7.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.6.0 → solixble-3.7.0}/PKG-INFO +1 -1
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/device.py +42 -41
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/solarbank2.py +151 -11
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/prime_device.py +1 -1
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/states.py +75 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE.egg-info/PKG-INFO +1 -1
- {solixble-3.6.0 → solixble-3.7.0}/pyproject.toml +1 -1
- {solixble-3.6.0 → solixble-3.7.0}/tests/test_devices.py +61 -0
- {solixble-3.6.0 → solixble-3.7.0}/LICENSE.txt +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/README.md +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/__init__.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/const.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/__init__.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/c1000.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/c1000g2.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/c300.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/c300dc.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/c800.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/f2000.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/f3800.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/generic.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/prime_charger_160w.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/prime_charger_250w.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/devices/solarbank3.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE/utilities.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE.egg-info/SOURCES.txt +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE.egg-info/dependency_links.txt +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE.egg-info/requires.txt +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/SolixBLE.egg-info/top_level.txt +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/setup.cfg +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/tests/test_connection.py +0 -0
- {solixble-3.6.0 → solixble-3.7.0}/tests/test_prime.py +0 -0
|
@@ -62,8 +62,8 @@ class SolixBLEDevice:
|
|
|
62
62
|
|
|
63
63
|
self._ble_device: BLEDevice = ble_device
|
|
64
64
|
self._client: BleakClient | None = None
|
|
65
|
-
self.
|
|
66
|
-
self.
|
|
65
|
+
self._fragment_buffers: dict[bytes, dict[int, bytes]] = {}
|
|
66
|
+
self._fragment_totals: dict[bytes, int] = {}
|
|
67
67
|
self._data: dict[str, bytes] | None = None
|
|
68
68
|
self._last_data_timestamp: datetime | None = None
|
|
69
69
|
self._last_packet_timestamp: datetime | None = None
|
|
@@ -347,11 +347,6 @@ class SolixBLEDevice:
|
|
|
347
347
|
# Extract command
|
|
348
348
|
packet_cmd = bytes([packet_copy.pop(0), packet_copy.pop(0)])
|
|
349
349
|
|
|
350
|
-
# Telemetry packets have an extra field which must be popped
|
|
351
|
-
if packet_pattern.hex() == "03010f" and packet_cmd.hex() == "c402":
|
|
352
|
-
special_value = bytes([packet_copy.pop(0)])
|
|
353
|
-
_LOGGER.debug(f"Special value: {special_value.hex()}")
|
|
354
|
-
|
|
355
350
|
# Extract payload
|
|
356
351
|
packet_payload = bytes(packet_copy)
|
|
357
352
|
|
|
@@ -498,7 +493,7 @@ class SolixBLEDevice:
|
|
|
498
493
|
)
|
|
499
494
|
return cipher.encrypt(padded_data)
|
|
500
495
|
|
|
501
|
-
async def _process_telemetry_packet(self, payload: bytes) -> None:
|
|
496
|
+
async def _process_telemetry_packet(self, payload: bytes, cmd: bytes = None) -> None:
|
|
502
497
|
"""Process a telemetry packet from the device.
|
|
503
498
|
|
|
504
499
|
This performs the default processing of telemetry packets in which
|
|
@@ -507,40 +502,46 @@ class SolixBLEDevice:
|
|
|
507
502
|
telemetry.
|
|
508
503
|
"""
|
|
509
504
|
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
505
|
+
# First byte encodes fragment info (high nibble = index, low = total)
|
|
506
|
+
fragment_index = (payload[0] >> 4) & 0x0F
|
|
507
|
+
fragment_total = payload[0] & 0x0F
|
|
522
508
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
509
|
+
# Multi-part message
|
|
510
|
+
if fragment_total > 1:
|
|
511
|
+
fragment_data = payload[1:]
|
|
512
|
+
cmd_key = bytes(cmd)
|
|
513
|
+
_LOGGER.debug(
|
|
514
|
+
f"Fragment {fragment_index}/{fragment_total} for cmd {cmd.hex()}, {len(fragment_data)} bytes"
|
|
526
515
|
)
|
|
527
516
|
|
|
528
|
-
|
|
529
|
-
self.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
_LOGGER.debug("Missing other payload!")
|
|
533
|
-
return
|
|
517
|
+
# Store fragment
|
|
518
|
+
if cmd_key not in self._fragment_buffers or fragment_index == 1:
|
|
519
|
+
self._fragment_buffers[cmd_key] = {}
|
|
520
|
+
self._fragment_totals[cmd_key] = fragment_total
|
|
534
521
|
|
|
535
|
-
|
|
522
|
+
self._fragment_buffers[cmd_key][fragment_index] = fragment_data
|
|
536
523
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
524
|
+
# Wait until all fragments have arrived
|
|
525
|
+
if len(self._fragment_buffers[cmd_key]) < fragment_total:
|
|
526
|
+
_LOGGER.debug("Waiting for remaining fragments...")
|
|
527
|
+
return
|
|
541
528
|
|
|
542
|
-
|
|
543
|
-
|
|
529
|
+
# Reassemble in order
|
|
530
|
+
payload = b"".join(
|
|
531
|
+
self._fragment_buffers[cmd_key][i]
|
|
532
|
+
for i in sorted(self._fragment_buffers[cmd_key])
|
|
533
|
+
)
|
|
534
|
+
del self._fragment_buffers[cmd_key]
|
|
535
|
+
del self._fragment_totals[cmd_key]
|
|
536
|
+
_LOGGER.debug(
|
|
537
|
+
f"Reassembled payload: {len(payload)} bytes"
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
else:
|
|
541
|
+
# Strip fragment info
|
|
542
|
+
payload = payload[1:]
|
|
543
|
+
|
|
544
|
+
decrypted_payload = self._decrypt_payload(payload)
|
|
544
545
|
_LOGGER.debug(f"Decrypted payload: {decrypted_payload.hex()}")
|
|
545
546
|
parameters = self._parse_payload(decrypted_payload)
|
|
546
547
|
return await self._process_telemetry(parameters)
|
|
@@ -584,7 +585,7 @@ class SolixBLEDevice:
|
|
|
584
585
|
) -> None:
|
|
585
586
|
"""Process a notification from the device."""
|
|
586
587
|
|
|
587
|
-
_LOGGER.debug(f"The client the notification is from
|
|
588
|
+
_LOGGER.debug(f"The client the notification is from: {client}")
|
|
588
589
|
|
|
589
590
|
if self._client is not client:
|
|
590
591
|
_LOGGER.debug("Ignoring notification from old client")
|
|
@@ -625,9 +626,9 @@ class SolixBLEDevice:
|
|
|
625
626
|
match cmd.hex():
|
|
626
627
|
|
|
627
628
|
# Telemetry messages
|
|
628
|
-
case "c402" | "4300":
|
|
629
|
+
case "c402" | "4300" | "c405":
|
|
629
630
|
_LOGGER.debug("Received telemetry message!")
|
|
630
|
-
return await self._process_telemetry_packet(payload)
|
|
631
|
+
return await self._process_telemetry_packet(payload, cmd)
|
|
631
632
|
|
|
632
633
|
# Unknown messages
|
|
633
634
|
case _:
|
|
@@ -1041,8 +1042,8 @@ class SolixBLEDevice:
|
|
|
1041
1042
|
self._data = None
|
|
1042
1043
|
self._last_data_timestamp = None
|
|
1043
1044
|
|
|
1044
|
-
self.
|
|
1045
|
-
self.
|
|
1045
|
+
self._fragment_buffers = {}
|
|
1046
|
+
self._fragment_totals = {}
|
|
1046
1047
|
self._shared_secret = None
|
|
1047
1048
|
self._last_packet_timestamp = None
|
|
1048
1049
|
self._negotiation_timestamp = None
|
|
@@ -4,8 +4,38 @@
|
|
|
4
4
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from ..const import (
|
|
10
|
+
DEFAULT_METADATA_BOOL,
|
|
11
|
+
DEFAULT_METADATA_FLOAT,
|
|
12
|
+
DEFAULT_METADATA_STRING,
|
|
13
|
+
)
|
|
8
14
|
from ..device import SolixBLEDevice
|
|
15
|
+
from ..states import GridStatus, LightMode, SBPowerCutoff, SBUsageMode, TemperatureUnit
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MaxLoadSB2(Enum):
|
|
19
|
+
"""
|
|
20
|
+
Maximum output power of the Solarbank 2 in watts.
|
|
21
|
+
|
|
22
|
+
Only specific values are allowed.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
#: The maximum load is unknown.
|
|
26
|
+
UNKNOWN = -1
|
|
27
|
+
|
|
28
|
+
#: 350 watts.
|
|
29
|
+
W350 = 350
|
|
30
|
+
|
|
31
|
+
#: 600 watts.
|
|
32
|
+
W600 = 600
|
|
33
|
+
|
|
34
|
+
#: 800 watts.
|
|
35
|
+
W800 = 800
|
|
36
|
+
|
|
37
|
+
#: 1000 watts.
|
|
38
|
+
W1000 = 1000
|
|
9
39
|
|
|
10
40
|
|
|
11
41
|
class Solarbank2(SolixBLEDevice):
|
|
@@ -15,10 +45,6 @@ class Solarbank2(SolixBLEDevice):
|
|
|
15
45
|
Use this class to connect and monitor a Solarbank 2 power station.
|
|
16
46
|
This model is also known as the A17C1.
|
|
17
47
|
|
|
18
|
-
.. note::
|
|
19
|
-
This model was added using data from anker-solix-api. It has not been
|
|
20
|
-
tested!
|
|
21
|
-
|
|
22
48
|
.. note::
|
|
23
49
|
It should be possible to add more sensors. I think devices with lots of
|
|
24
50
|
telemetry values split them up into multiple messages but I have not
|
|
@@ -132,9 +158,9 @@ class Solarbank2(SolixBLEDevice):
|
|
|
132
158
|
|
|
133
159
|
@property
|
|
134
160
|
def pv_yield(self) -> float:
|
|
135
|
-
"""Solar
|
|
161
|
+
"""Solar energy generated in kWh.
|
|
136
162
|
|
|
137
|
-
:returns: Total solar
|
|
163
|
+
:returns: Total solar energy generated or default float value.
|
|
138
164
|
"""
|
|
139
165
|
if self._data is None:
|
|
140
166
|
return DEFAULT_METADATA_FLOAT
|
|
@@ -143,18 +169,20 @@ class Solarbank2(SolixBLEDevice):
|
|
|
143
169
|
|
|
144
170
|
@property
|
|
145
171
|
def charged_energy(self) -> float:
|
|
146
|
-
"""
|
|
172
|
+
"""Total accumulated energy that passed through the battery in kWh
|
|
147
173
|
|
|
148
|
-
:returns:
|
|
174
|
+
:returns: The amount of energy or default float value.
|
|
149
175
|
"""
|
|
150
176
|
if self._data is None:
|
|
151
177
|
return DEFAULT_METADATA_FLOAT
|
|
152
178
|
|
|
153
|
-
|
|
179
|
+
# The / 100 000 is correct despite all other divisors being 10 000.
|
|
180
|
+
# This is the "Storage" stats field in the Anker app
|
|
181
|
+
return self._parse_int("b2", begin=1) / 100000.0
|
|
154
182
|
|
|
155
183
|
@property
|
|
156
184
|
def output_energy(self) -> float:
|
|
157
|
-
"""Output energy.
|
|
185
|
+
"""Output energy in kWh.
|
|
158
186
|
|
|
159
187
|
:returns: Total energy output or default float value.
|
|
160
188
|
"""
|
|
@@ -305,3 +333,115 @@ class Solarbank2(SolixBLEDevice):
|
|
|
305
333
|
return DEFAULT_METADATA_FLOAT
|
|
306
334
|
|
|
307
335
|
return self._parse_int("d3", begin=1) / 10.0
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def error_code(self) -> int:
|
|
339
|
+
"""Device error code.
|
|
340
|
+
|
|
341
|
+
:returns: Error code or default int value.
|
|
342
|
+
"""
|
|
343
|
+
return self._parse_int("a5", begin=1)
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def temperature_unit(self) -> TemperatureUnit:
|
|
347
|
+
"""Temperature unit setting.
|
|
348
|
+
|
|
349
|
+
:returns: Temperature unit (Celsius or Fahrenheit).
|
|
350
|
+
"""
|
|
351
|
+
return TemperatureUnit(self._parse_int("a9", begin=1))
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def output_cutoff_data(self) -> SBPowerCutoff:
|
|
355
|
+
"""
|
|
356
|
+
Output cutoff threshold in %.
|
|
357
|
+
|
|
358
|
+
Minimum battery SOC to maintain.
|
|
359
|
+
|
|
360
|
+
:returns: Output cutoff battery SOC threshold.
|
|
361
|
+
"""
|
|
362
|
+
return SBPowerCutoff(self._parse_int("b4", begin=1))
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def lowpower_input_data(self) -> int:
|
|
366
|
+
"""Low power input data.
|
|
367
|
+
|
|
368
|
+
:returns: Low power input data or default int value.
|
|
369
|
+
"""
|
|
370
|
+
return self._parse_int("b5", begin=1)
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def input_cutoff_data(self) -> SBPowerCutoff:
|
|
374
|
+
"""Input cutoff threshold in %.
|
|
375
|
+
|
|
376
|
+
:returns: Input cutoff battery SOC threshold.
|
|
377
|
+
"""
|
|
378
|
+
return SBPowerCutoff(self._parse_int("b6", begin=1))
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def max_load(self) -> MaxLoadSB2:
|
|
382
|
+
"""
|
|
383
|
+
Maximum output power in watts.
|
|
384
|
+
|
|
385
|
+
Maximum legal value depends on country of operation.
|
|
386
|
+
|
|
387
|
+
:returns: Maximum load as a MaxLoadSB2 enum value.
|
|
388
|
+
"""
|
|
389
|
+
return MaxLoadSB2(self._parse_int("c2", begin=1))
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def usage_mode(self) -> SBUsageMode:
|
|
393
|
+
"""Usage mode.
|
|
394
|
+
|
|
395
|
+
:returns: Usage mode as a SBUsageMode enum value.
|
|
396
|
+
"""
|
|
397
|
+
return SBUsageMode(self._parse_int("c6", begin=1))
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def home_load_preset(self) -> int:
|
|
401
|
+
"""Home load preset in watts.
|
|
402
|
+
|
|
403
|
+
:returns: Home load preset in watts or default int value.
|
|
404
|
+
"""
|
|
405
|
+
return self._parse_int("c7", begin=1)
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def light_mode(self) -> LightMode:
|
|
409
|
+
"""Light mode. Normal or Mood.
|
|
410
|
+
|
|
411
|
+
:returns: Light mode.
|
|
412
|
+
"""
|
|
413
|
+
return LightMode(self._parse_int("d2", begin=1))
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def grid_status(self) -> GridStatus:
|
|
417
|
+
"""Grid connection status.
|
|
418
|
+
|
|
419
|
+
:returns: Grid status.
|
|
420
|
+
"""
|
|
421
|
+
return GridStatus(self._parse_int("e0", begin=1))
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def light_on(self) -> bool | None:
|
|
425
|
+
"""Whether the light is switched on.
|
|
426
|
+
Original value is inverted because it is called "light_off_switch"
|
|
427
|
+
|
|
428
|
+
:returns: True if light is on, False if off.
|
|
429
|
+
"""
|
|
430
|
+
return (
|
|
431
|
+
not bool(self._parse_int("e1", begin=1))
|
|
432
|
+
if self._data is not None
|
|
433
|
+
else DEFAULT_METADATA_BOOL
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def battery_heating(self) -> bool | None:
|
|
438
|
+
"""Whether the battery is currently heating.
|
|
439
|
+
|
|
440
|
+
:returns: True if heating, False if not heating.
|
|
441
|
+
"""
|
|
442
|
+
return (
|
|
443
|
+
bool(self._parse_int("e8", begin=1))
|
|
444
|
+
if self._data is not None
|
|
445
|
+
else DEFAULT_METADATA_BOOL
|
|
446
|
+
)
|
|
447
|
+
|
|
@@ -487,7 +487,7 @@ class PrimeDevice(SolixBLEDevice):
|
|
|
487
487
|
# Packet processing #
|
|
488
488
|
#####################
|
|
489
489
|
|
|
490
|
-
async def _process_telemetry_packet(self, payload: bytes) -> None:
|
|
490
|
+
async def _process_telemetry_packet(self, payload: bytes, cmd: bytes = None) -> None:
|
|
491
491
|
"""
|
|
492
492
|
Process a telemetry packet from an Anker Prime device.
|
|
493
493
|
|
|
@@ -90,6 +90,19 @@ class LightStatus(Enum):
|
|
|
90
90
|
SOS = 4
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
class LightMode(Enum):
|
|
94
|
+
"""The light mode of the device."""
|
|
95
|
+
|
|
96
|
+
#: The light mode is unknown.
|
|
97
|
+
UNKNOWN = -1
|
|
98
|
+
|
|
99
|
+
#: Normal light mode.
|
|
100
|
+
NORMAL = 0
|
|
101
|
+
|
|
102
|
+
#: Mood light mode.
|
|
103
|
+
MOOD = 1
|
|
104
|
+
|
|
105
|
+
|
|
93
106
|
class DisplayTimeout(Enum):
|
|
94
107
|
"""Display timeout on device in seconds. Only specific values are allowed."""
|
|
95
108
|
|
|
@@ -123,6 +136,68 @@ class TemperatureUnit(Enum):
|
|
|
123
136
|
#: Display unit is Fahrenheit.
|
|
124
137
|
FAHRENHEIT = 1
|
|
125
138
|
|
|
139
|
+
class GridStatus(Enum):
|
|
140
|
+
"""The grid connection status."""
|
|
141
|
+
|
|
142
|
+
#: The grid status is unknown.
|
|
143
|
+
UNKNOWN = -1
|
|
144
|
+
|
|
145
|
+
#: Grid is connected and OK.
|
|
146
|
+
OK = 1
|
|
147
|
+
|
|
148
|
+
#: Undocumented in API, but device operates as expected and
|
|
149
|
+
#: outputs power to grid. Maybe a pure "dispense" state because
|
|
150
|
+
#: SB2 can't draw power from the grid
|
|
151
|
+
OK_AS_WELL_I_GUESS = 2
|
|
152
|
+
|
|
153
|
+
#: Grid is connecting.
|
|
154
|
+
CONNECTING = 3
|
|
155
|
+
|
|
156
|
+
#: No grid connection.
|
|
157
|
+
NO_GRID = 6
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SBUsageMode(Enum):
|
|
161
|
+
"""Usage mode of a Solarbank device."""
|
|
162
|
+
|
|
163
|
+
#: The usage mode is unknown.
|
|
164
|
+
UNKNOWN = -1
|
|
165
|
+
|
|
166
|
+
#: Manual (schedule) mode.
|
|
167
|
+
MANUAL = 1
|
|
168
|
+
|
|
169
|
+
#: Smart meter mode.
|
|
170
|
+
SMARTMETER = 2
|
|
171
|
+
|
|
172
|
+
#: Smart plugs mode.
|
|
173
|
+
SMARTPLUGS = 3
|
|
174
|
+
|
|
175
|
+
#: Backup mode.
|
|
176
|
+
BACKUP = 4
|
|
177
|
+
|
|
178
|
+
#: Use time mode.
|
|
179
|
+
USE_TIME = 5
|
|
180
|
+
|
|
181
|
+
#: Smart mode.
|
|
182
|
+
SMART = 7
|
|
183
|
+
|
|
184
|
+
#: Time slot mode.
|
|
185
|
+
TIME_SLOT = 8
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class SBPowerCutoff(Enum):
|
|
189
|
+
"""Power cutoff threshold of a Solarbank device in %."""
|
|
190
|
+
|
|
191
|
+
#: The cutoff threshold is unknown.
|
|
192
|
+
UNKNOWN = -1
|
|
193
|
+
|
|
194
|
+
#: 5 %.
|
|
195
|
+
P5 = 5
|
|
196
|
+
|
|
197
|
+
#: 10 %.
|
|
198
|
+
P10 = 10
|
|
199
|
+
|
|
200
|
+
|
|
126
201
|
class PortOverload(Enum):
|
|
127
202
|
"""The overload status of a port."""
|
|
128
203
|
|
|
@@ -22,9 +22,12 @@ from SolixBLE import (
|
|
|
22
22
|
PortStatus,
|
|
23
23
|
PrimeCharger160w,
|
|
24
24
|
PrimeDevice,
|
|
25
|
+
Solarbank2,
|
|
25
26
|
SolixBLEDevice,
|
|
26
27
|
TemperatureUnit,
|
|
27
28
|
)
|
|
29
|
+
from SolixBLE.devices.solarbank2 import MaxLoadSB2
|
|
30
|
+
from SolixBLE.states import GridStatus, LightMode, SBPowerCutoff, SBUsageMode
|
|
28
31
|
from tests.const import (
|
|
29
32
|
MOCK_BLE_DEVICE,
|
|
30
33
|
NEGOTIATION_RESPONSES_PRIME,
|
|
@@ -614,6 +617,51 @@ from tests.helpers import MockDevice
|
|
|
614
617
|
},
|
|
615
618
|
id="c300_dc_mixed_values",
|
|
616
619
|
),
|
|
620
|
+
pytest.param(
|
|
621
|
+
Solarbank2,
|
|
622
|
+
"a10131a2110041504347513830453030303030303030a302013aa4020101a503020000a605030100060aa7050300000631a8050300030306a9020100aa020111ab050300000000ac0503f4010000ad02013aae020100af020100b0050300000000b10503e0bd0200b20503723c0a00b305038d840200b4020105b5020104b6020105b7050388130000b8020101b9020100ba050328000000bb020100bc050300000000bd050300000000be050300000000bf050300000000c0110000000000000000000000000000000000c1020100c203022003c40503f4010000c5020100c6020101c703023200c8050300000000c9050306000000ca050300000000cb050300000000cc050300000000cd050300000000d2020100d30503f4010000d4110000000000000000000000000000000000d503020000d6110000000000000000000000000000000000d703020000d8110000000000000000000000000000000000d903020000da110000000000000000000000000000000000db03020000dc110000000000000000000000000000000000dd03020000de110000000000000000000000000000000000df03020000e0020102e1020101e2020100e3020100e4020100e5020100e6020100e7020100e8020100e9020100ea020101fe05039a46d969fb050300000000fc1604010101010001010101010100000000000000000000",
|
|
623
|
+
{
|
|
624
|
+
"serial_number": "APCGQ80E00000000",
|
|
625
|
+
"battery_percentage": 58,
|
|
626
|
+
"battery_percentage_aggregate": 58,
|
|
627
|
+
"error_code": 0,
|
|
628
|
+
"software_version": "1.6.8.1.6.5.3.7.7",
|
|
629
|
+
"software_version_controller": "8.2.2.4.7.6.8.0.0",
|
|
630
|
+
"software_version_expansion": "1.0.0.8.6.0.6.7.2",
|
|
631
|
+
"temperature_unit": TemperatureUnit.CELSIUS,
|
|
632
|
+
"temperature": 17,
|
|
633
|
+
"solar_power_in": 0.0,
|
|
634
|
+
"solar_pv_1_power_in": 0.0,
|
|
635
|
+
"solar_pv_2_power_in": 0.0,
|
|
636
|
+
"solar_pv_3_power_in": 0.0,
|
|
637
|
+
"solar_pv_4_power_in": 0.0,
|
|
638
|
+
"ac_power_out": 50.0,
|
|
639
|
+
"ac_power_out_sockets": 0.0,
|
|
640
|
+
"battery_charge_power": 0.0,
|
|
641
|
+
"battery_discharge_power": 50.0,
|
|
642
|
+
"pv_yield": 17.968,
|
|
643
|
+
"charged_energy": 6.70834,
|
|
644
|
+
"output_energy": 16.5005,
|
|
645
|
+
"grid_to_home_power": 0.0,
|
|
646
|
+
"pv_to_grid_power": 0.0,
|
|
647
|
+
"grid_import_energy": 0.0,
|
|
648
|
+
"grid_export_energy": 0.0,
|
|
649
|
+
"house_demand": 50.0,
|
|
650
|
+
"consumed_energy": 0.0006,
|
|
651
|
+
"power_out": 50.0,
|
|
652
|
+
"max_load": MaxLoadSB2.W800,
|
|
653
|
+
"output_cutoff_data": SBPowerCutoff.P5,
|
|
654
|
+
"lowpower_input_data": 4,
|
|
655
|
+
"input_cutoff_data": SBPowerCutoff.P5,
|
|
656
|
+
"usage_mode": SBUsageMode.MANUAL,
|
|
657
|
+
"home_load_preset": 50,
|
|
658
|
+
"light_mode": LightMode.NORMAL,
|
|
659
|
+
"grid_status": GridStatus.OK_AS_WELL_I_GUESS,
|
|
660
|
+
"light_on": False,
|
|
661
|
+
"battery_heating": False,
|
|
662
|
+
},
|
|
663
|
+
id="solarbank2_telemetry",
|
|
664
|
+
),
|
|
617
665
|
],
|
|
618
666
|
)
|
|
619
667
|
async def test_values(
|
|
@@ -692,6 +740,19 @@ async def test_values(
|
|
|
692
740
|
"0c4d9db9ef376fcfe627b9b73089eda514315d4bf67fb7eb299f2894ef7a059c",
|
|
693
741
|
id="c1000_2",
|
|
694
742
|
),
|
|
743
|
+
pytest.param(
|
|
744
|
+
Solarbank2,
|
|
745
|
+
[
|
|
746
|
+
"ff090e00030001080100a1010152",
|
|
747
|
+
"ff091b00030001080300a10102a202fd00a30144a40101a50102ff",
|
|
748
|
+
"ff093800030001082900a10103a2054553503332a307302e302e302e33a41041504347513830453030303030303030a50600000000000039",
|
|
749
|
+
"ff090b00030001080500f2",
|
|
750
|
+
"ff094d00030001082100a140f809d676751fba1346f21198c8a583b1ef9b9a617fb804455c388d07090e6dc2976c1bb1cf06aee1f30a3286af9dd80f8f0c594010f60755292addedfe41385972",
|
|
751
|
+
None,
|
|
752
|
+
],
|
|
753
|
+
"6a2c89888de58cce1e15d98eb22669898ec29bcb1519ce19f950439aac9dbcb5",
|
|
754
|
+
id="solarbank2_1",
|
|
755
|
+
),
|
|
695
756
|
],
|
|
696
757
|
)
|
|
697
758
|
async def test_negotiation(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|