goodwe 0.3.2__tar.gz → 0.3.3__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.
- {goodwe-0.3.2/goodwe.egg-info → goodwe-0.3.3}/PKG-INFO +1 -1
- goodwe-0.3.3/VERSION +1 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/es.py +1 -1
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/et.py +18 -4
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/inverter.py +2 -2
- goodwe-0.3.3/goodwe/model.py +50 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/sensor.py +72 -13
- {goodwe-0.3.2 → goodwe-0.3.3/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.3.2 → goodwe-0.3.3}/tests/test_dt.py +4 -4
- {goodwe-0.3.2 → goodwe-0.3.3}/tests/test_et.py +4 -4
- {goodwe-0.3.2 → goodwe-0.3.3}/tests/test_sensor.py +13 -2
- goodwe-0.3.2/VERSION +0 -1
- goodwe-0.3.2/goodwe/model.py +0 -40
- {goodwe-0.3.2 → goodwe-0.3.3}/LICENSE +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/README.md +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/__init__.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/const.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/dt.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/exceptions.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/modbus.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe/protocol.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/pyproject.toml +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/setup.cfg +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/tests/test_es.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/tests/test_modbus.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.3}/tests/test_protocol.py +0 -0
goodwe-0.3.3/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.3
|
|
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ES(Inverter):
|
|
18
|
-
"""Class representing inverter of ES/EM/BP family"""
|
|
18
|
+
"""Class representing inverter of ES/EM/BP family AKA platform 105"""
|
|
19
19
|
|
|
20
20
|
_READ_DEVICE_VERSION_INFO: ProtocolCommand = Aa55ProtocolCommand("010200", "0182")
|
|
21
21
|
_READ_DEVICE_RUNNING_DATA: ProtocolCommand = Aa55ProtocolCommand("010600", "0186")
|
|
@@ -7,7 +7,7 @@ from .exceptions import RequestRejectedException
|
|
|
7
7
|
from .inverter import Inverter
|
|
8
8
|
from .inverter import OperationMode
|
|
9
9
|
from .inverter import SensorKind as Kind
|
|
10
|
-
from .model import is_2_battery, is_4_mppt, is_single_phase
|
|
10
|
+
from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
|
|
11
11
|
from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
|
|
12
12
|
from .sensor import *
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ET(Inverter):
|
|
18
|
-
"""Class representing inverter of ET/EH/BT/BH or GE's GEH families"""
|
|
18
|
+
"""Class representing inverter of ET/EH/BT/BH or GE's GEH families AKA platform 205 or 745"""
|
|
19
19
|
|
|
20
20
|
# Modbus registers from offset 0x891c (35100), count 0x7d (125)
|
|
21
21
|
__all_sensors: Tuple[Sensor, ...] = (
|
|
@@ -384,19 +384,25 @@ class ET(Inverter):
|
|
|
384
384
|
|
|
385
385
|
Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
|
|
386
386
|
Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
|
|
387
|
-
Integer("load_control_soc",
|
|
387
|
+
Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
|
|
388
388
|
|
|
389
389
|
Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
|
|
390
390
|
)
|
|
391
391
|
|
|
392
392
|
# Settings added in ARM firmware 22
|
|
393
393
|
__settings_arm_fw_22: Tuple[Sensor, ...] = (
|
|
394
|
+
Long("peak_shaving_power_limit", 47542, "Peak Shaving Power Limit"),
|
|
395
|
+
Integer("peak_shaving_soc", 47544, "Peak Shaving SoC"),
|
|
394
396
|
# EcoModeV2("eco_modeV2_5", 47571, "Eco Mode Version 2 Power Group 5"),
|
|
395
397
|
# EcoModeV2("eco_modeV2_6", 47577, "Eco Mode Version 2 Power Group 6"),
|
|
396
398
|
# EcoModeV2("eco_modeV2_7", 47583, "Eco Mode Version 2 Power Group 7"),
|
|
397
399
|
PeakShavingMode("peak_shaving_mode", 47589, "Peak Shaving Mode"),
|
|
398
400
|
|
|
399
401
|
Integer("dod_holding", 47602, "DoD Holding", "", Kind.BAT),
|
|
402
|
+
Integer("backup_mode_enable", 47605, "Backup Mode Switch"),
|
|
403
|
+
Integer("max_charge_power", 47606, "Max Charge Power"),
|
|
404
|
+
Integer("smart_charging_enable", 47609, "Smart Charging Mode Switch"),
|
|
405
|
+
Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
|
|
400
406
|
)
|
|
401
407
|
|
|
402
408
|
def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
|
|
@@ -626,6 +632,7 @@ class ET(Inverter):
|
|
|
626
632
|
await self._set_offline(True)
|
|
627
633
|
await self.write_setting('backup_supply', 1)
|
|
628
634
|
await self.write_setting('cold_start', 4)
|
|
635
|
+
await self._clear_battery_mode_param()
|
|
629
636
|
elif operation_mode == OperationMode.BACKUP:
|
|
630
637
|
await self.write_setting('work_mode', 2)
|
|
631
638
|
await self._set_offline(False)
|
|
@@ -636,13 +643,20 @@ class ET(Inverter):
|
|
|
636
643
|
elif operation_mode == OperationMode.PEAK_SHAVING:
|
|
637
644
|
await self.write_setting('work_mode', 4)
|
|
638
645
|
await self._set_offline(False)
|
|
646
|
+
await self._clear_battery_mode_param()
|
|
639
647
|
elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE):
|
|
640
648
|
if eco_mode_power < 0 or eco_mode_power > 100:
|
|
641
649
|
raise ValueError()
|
|
642
650
|
if eco_mode_soc < 0 or eco_mode_soc > 100:
|
|
643
651
|
raise ValueError()
|
|
652
|
+
|
|
644
653
|
eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
|
|
645
|
-
|
|
654
|
+
# Load the current values to try to detect schedule type
|
|
655
|
+
try:
|
|
656
|
+
await self._read_setting(eco_mode)
|
|
657
|
+
except ValueError:
|
|
658
|
+
pass
|
|
659
|
+
eco_mode.set_schedule_type(ScheduleType.ECO_MODE, is_745_platform(self))
|
|
646
660
|
if operation_mode == OperationMode.ECO_CHARGE:
|
|
647
661
|
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
|
|
648
662
|
else:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Serial number tags to identify inverter type
|
|
2
|
+
from .inverter import Inverter
|
|
3
|
+
|
|
4
|
+
PLATFORM_105_MODELS = ("ESU", "EMU", "ESA", "BPS", "BPU", "EMJ", "IJL")
|
|
5
|
+
PLATFORM_205_MODELS = ("ETU", "ETL", "ETR", "BHN", "EHU", "BHU", "EHR", "BTU")
|
|
6
|
+
PLATFORM_745_LV_MODELS = ("ESN", "EBN", "EMN", "SPN", "ERN", "ESC", "HLB", "HMB", "HBB", "EOA")
|
|
7
|
+
PLATFORM_745_HV_MODELS = ("ETT", "HTA", "HUB", "AEB", "SPB", "CUB", "EUB", "HEB", "ERB", "BTT", "ETF", "ARB", "URB",
|
|
8
|
+
"EBR")
|
|
9
|
+
PLATFORM_753_MODELS = ("AES", "HHI", "ABP", "EHB", "HSB", "HUA", "CUA")
|
|
10
|
+
|
|
11
|
+
ET_MODEL_TAGS = PLATFORM_205_MODELS + PLATFORM_745_LV_MODELS + PLATFORM_745_HV_MODELS + PLATFORM_753_MODELS + (
|
|
12
|
+
"ETC", "BTC", "BTN") # Qianhai
|
|
13
|
+
ES_MODEL_TAGS = PLATFORM_105_MODELS
|
|
14
|
+
DT_MODEL_TAGS = ("DTU", "DTS",
|
|
15
|
+
"MSU", "MST", "MSC", "DSN", "DTN", "DST", "NSU", "SSN", "SST", "SSX", "SSY",
|
|
16
|
+
"PSB", "PSC")
|
|
17
|
+
|
|
18
|
+
SINGLE_PHASE_MODELS = ("DSN", "DST", "NSU", "SSN", "SST", "SSX", "SSY", # DT
|
|
19
|
+
"MSU", "MST", "PSB", "PSC",
|
|
20
|
+
"MSC", # Found on third gen MS
|
|
21
|
+
"EHU", "EHR", "HSB", # ET
|
|
22
|
+
"ESN", "EMN", "ERN", "EBN", "HLB", "HMB", "HBB", "SPN") # ES Gen 2
|
|
23
|
+
|
|
24
|
+
MPPT3_MODELS = ("MSU", "MST", "PSC", "MSC",
|
|
25
|
+
"25KET", "29K9ET")
|
|
26
|
+
|
|
27
|
+
MPPT4_MODELS = ("HSB",)
|
|
28
|
+
|
|
29
|
+
BAT_2_MODELS = ("25KET", "29K9ET")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_single_phase(inverter: Inverter) -> bool:
|
|
33
|
+
return any(model in inverter.serial_number for model in SINGLE_PHASE_MODELS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_3_mppt(inverter: Inverter) -> bool:
|
|
37
|
+
return any(model in inverter.serial_number for model in MPPT3_MODELS)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_4_mppt(inverter: Inverter) -> bool:
|
|
41
|
+
return any(model in inverter.serial_number for model in MPPT4_MODELS)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_2_battery(inverter: Inverter) -> bool:
|
|
45
|
+
return any(model in inverter.serial_number for model in BAT_2_MODELS)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_745_platform(inverter: Inverter) -> bool:
|
|
49
|
+
return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any(
|
|
50
|
+
model in inverter.serial_number for model in PLATFORM_745_HV_MODELS)
|
|
@@ -21,12 +21,13 @@ class ScheduleType(IntEnum):
|
|
|
21
21
|
PEAK_SHAVING = 3,
|
|
22
22
|
BACKUP_MODE = 4,
|
|
23
23
|
SMART_CHARGE_MODE = 5,
|
|
24
|
-
ECO_MODE_745 = 6
|
|
24
|
+
ECO_MODE_745 = 6,
|
|
25
|
+
NOT_SET = 85
|
|
25
26
|
|
|
26
27
|
@classmethod
|
|
27
28
|
def detect_schedule_type(cls, value: int) -> ScheduleType:
|
|
28
29
|
"""Detect schedule type from its on/off value"""
|
|
29
|
-
if value in (0, -1
|
|
30
|
+
if value in (0, -1):
|
|
30
31
|
return ScheduleType.ECO_MODE
|
|
31
32
|
elif value in (1, -2):
|
|
32
33
|
return ScheduleType.DRY_CONTACT_LOAD
|
|
@@ -40,6 +41,8 @@ class ScheduleType(IntEnum):
|
|
|
40
41
|
return ScheduleType.SMART_CHARGE_MODE
|
|
41
42
|
elif value in (6, -7):
|
|
42
43
|
return ScheduleType.ECO_MODE_745
|
|
44
|
+
elif value == 85:
|
|
45
|
+
return ScheduleType.NOT_SET
|
|
43
46
|
else:
|
|
44
47
|
raise ValueError(f"{value}: on_off value {value} out of range.")
|
|
45
48
|
|
|
@@ -52,12 +55,13 @@ class ScheduleType(IntEnum):
|
|
|
52
55
|
|
|
53
56
|
def decode_power(self, value: int) -> int:
|
|
54
57
|
"""Decode human readable value of power parameter"""
|
|
55
|
-
if self == ScheduleType.
|
|
56
|
-
return value
|
|
57
|
-
elif self == ScheduleType.PEAK_SHAVING:
|
|
58
|
+
if self == ScheduleType.PEAK_SHAVING:
|
|
58
59
|
return value * 10
|
|
59
|
-
|
|
60
|
+
elif self == ScheduleType.ECO_MODE_745:
|
|
60
61
|
return int(value / 10)
|
|
62
|
+
elif self == ScheduleType.NOT_SET:
|
|
63
|
+
# Prevent out of range values when changing mode
|
|
64
|
+
return value if -100 <= value <= 100 else int(value / 10)
|
|
61
65
|
else:
|
|
62
66
|
return value
|
|
63
67
|
|
|
@@ -67,7 +71,7 @@ class ScheduleType(IntEnum):
|
|
|
67
71
|
return value
|
|
68
72
|
elif self == ScheduleType.PEAK_SHAVING:
|
|
69
73
|
return int(value / 10)
|
|
70
|
-
|
|
74
|
+
elif self == ScheduleType.ECO_MODE_745:
|
|
71
75
|
return value * 10
|
|
72
76
|
else:
|
|
73
77
|
return value
|
|
@@ -76,7 +80,7 @@ class ScheduleType(IntEnum):
|
|
|
76
80
|
"""Check if the value fits in allowed values range"""
|
|
77
81
|
if self == ScheduleType.ECO_MODE:
|
|
78
82
|
return -100 <= value <= 100
|
|
79
|
-
|
|
83
|
+
elif self == ScheduleType.ECO_MODE_745:
|
|
80
84
|
return -1000 <= value <= 1000
|
|
81
85
|
else:
|
|
82
86
|
return True
|
|
@@ -298,6 +302,19 @@ class ByteL(Byte):
|
|
|
298
302
|
|
|
299
303
|
|
|
300
304
|
class Integer(Sensor):
|
|
305
|
+
"""Sensor representing unsigned int value encoded in 2 bytes"""
|
|
306
|
+
|
|
307
|
+
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
308
|
+
super().__init__(id_, offset, name, 2, unit, kind)
|
|
309
|
+
|
|
310
|
+
def read_value(self, data: ProtocolResponse):
|
|
311
|
+
return read_bytes2(data)
|
|
312
|
+
|
|
313
|
+
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
314
|
+
return int.to_bytes(int(value), length=2, byteorder="big", signed=False)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class IntegerS(Sensor):
|
|
301
318
|
"""Sensor representing signed int value encoded in 2 bytes"""
|
|
302
319
|
|
|
303
320
|
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
@@ -311,6 +328,19 @@ class Integer(Sensor):
|
|
|
311
328
|
|
|
312
329
|
|
|
313
330
|
class Long(Sensor):
|
|
331
|
+
"""Sensor representing unsigned int value encoded in 4 bytes"""
|
|
332
|
+
|
|
333
|
+
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
334
|
+
super().__init__(id_, offset, name, 4, unit, kind)
|
|
335
|
+
|
|
336
|
+
def read_value(self, data: ProtocolResponse):
|
|
337
|
+
return read_bytes4(data)
|
|
338
|
+
|
|
339
|
+
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
340
|
+
return int.to_bytes(int(value), length=4, byteorder="big", signed=False)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class LongS(Sensor):
|
|
314
344
|
"""Sensor representing signed int value encoded in 4 bytes"""
|
|
315
345
|
|
|
316
346
|
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
@@ -476,6 +506,14 @@ class EcoMode(ABC):
|
|
|
476
506
|
def is_eco_discharge_mode(self) -> bool:
|
|
477
507
|
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
|
|
478
508
|
|
|
509
|
+
@abstractmethod
|
|
510
|
+
def get_schedule_type(self) -> ScheduleType:
|
|
511
|
+
"""Answer the schedule type"""
|
|
512
|
+
|
|
513
|
+
@abstractmethod
|
|
514
|
+
def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
|
|
515
|
+
"""Set the schedule type"""
|
|
516
|
+
|
|
479
517
|
|
|
480
518
|
class EcoModeV1(Sensor, EcoMode):
|
|
481
519
|
"""Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
|
|
@@ -518,8 +556,6 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
518
556
|
raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
|
|
519
557
|
self.day_bits = read_byte(data)
|
|
520
558
|
self.days = decode_day_of_week(self.day_bits)
|
|
521
|
-
if self.day_bits < 0:
|
|
522
|
-
raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
|
|
523
559
|
return self
|
|
524
560
|
|
|
525
561
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
@@ -561,6 +597,14 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
561
597
|
and self.day_bits == 127 \
|
|
562
598
|
and self.power > 0
|
|
563
599
|
|
|
600
|
+
def get_schedule_type(self) -> ScheduleType:
|
|
601
|
+
"""Answer the schedule type"""
|
|
602
|
+
return ScheduleType.ECO_MODE
|
|
603
|
+
|
|
604
|
+
def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
|
|
605
|
+
"""Set the schedule type"""
|
|
606
|
+
pass
|
|
607
|
+
|
|
564
608
|
def as_eco_mode_v2(self) -> EcoModeV2:
|
|
565
609
|
"""Convert V1 to V2 EcoMode"""
|
|
566
610
|
result = EcoModeV2(self.id_, self.offset, self.name)
|
|
@@ -617,8 +661,6 @@ class Schedule(Sensor, EcoMode):
|
|
|
617
661
|
self.schedule_type = ScheduleType.detect_schedule_type(self.on_off)
|
|
618
662
|
self.day_bits = read_byte(data)
|
|
619
663
|
self.days = decode_day_of_week(self.day_bits)
|
|
620
|
-
if self.day_bits < 0:
|
|
621
|
-
raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
|
|
622
664
|
self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
|
|
623
665
|
if not self.schedule_type.is_in_range(self.power):
|
|
624
666
|
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
|
|
@@ -680,6 +722,19 @@ class Schedule(Sensor, EcoMode):
|
|
|
680
722
|
and self.power > 0 \
|
|
681
723
|
and (self.month_bits == 0 or self.month_bits == 0x0fff)
|
|
682
724
|
|
|
725
|
+
def get_schedule_type(self) -> ScheduleType:
|
|
726
|
+
"""Answer the schedule type"""
|
|
727
|
+
return self.schedule_type
|
|
728
|
+
|
|
729
|
+
def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
|
|
730
|
+
"""Set the schedule type"""
|
|
731
|
+
if schedule_type == ScheduleType.ECO_MODE:
|
|
732
|
+
# try to keep-reuse the type, use is745 only when necessary
|
|
733
|
+
if self.schedule_type not in (ScheduleType.ECO_MODE, ScheduleType.ECO_MODE_745):
|
|
734
|
+
self.schedule_type = ScheduleType.ECO_MODE_745 if is745 else ScheduleType.ECO_MODE
|
|
735
|
+
else:
|
|
736
|
+
self.schedule_type = schedule_type
|
|
737
|
+
|
|
683
738
|
def as_eco_mode_v1(self) -> EcoModeV1:
|
|
684
739
|
"""Convert V2 to V1 EcoMode"""
|
|
685
740
|
result = EcoModeV1(self.id_, self.offset, self.name)
|
|
@@ -891,6 +946,10 @@ def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
|
|
|
891
946
|
|
|
892
947
|
|
|
893
948
|
def decode_day_of_week(data: int) -> str:
|
|
949
|
+
if data == -1:
|
|
950
|
+
return "Mon-Sun"
|
|
951
|
+
elif data == 0:
|
|
952
|
+
return ""
|
|
894
953
|
bits = bin(data)[2:]
|
|
895
954
|
daynames = list(DAY_NAMES)
|
|
896
955
|
days = ""
|
|
@@ -904,7 +963,7 @@ def decode_day_of_week(data: int) -> str:
|
|
|
904
963
|
|
|
905
964
|
|
|
906
965
|
def decode_months(data: int) -> str | None:
|
|
907
|
-
if data
|
|
966
|
+
if data <= 0 or data == 0x0fff:
|
|
908
967
|
return None
|
|
909
968
|
bits = bin(data)[2:]
|
|
910
969
|
monthnames = list(MONTH_NAMES)
|
|
@@ -95,7 +95,7 @@ class GW6000_DT_Test(DtMock):
|
|
|
95
95
|
self.assertSensor('funbit', 0, '', data)
|
|
96
96
|
self.assertSensor('vbus', 601.2, 'V', data)
|
|
97
97
|
self.assertSensor('vnbus', 305.4, 'V', data)
|
|
98
|
-
self.assertSensor('derating_mode',
|
|
98
|
+
self.assertSensor('derating_mode', 0, '', data)
|
|
99
99
|
self.assertSensor('derating_mode_label', '', '', data)
|
|
100
100
|
|
|
101
101
|
self.assertFalse(self.sensor_map, f"Some sensors were not tested {self.sensor_map}")
|
|
@@ -169,7 +169,7 @@ class GW8K_DT_Test(DtMock):
|
|
|
169
169
|
self.assertSensor('temperature', 45.3, 'C', data)
|
|
170
170
|
self.assertSensor('e_day', 0.0, 'kWh', data)
|
|
171
171
|
self.assertSensor('e_total', 0.0, 'kWh', data)
|
|
172
|
-
self.assertSensor('h_total',
|
|
172
|
+
self.assertSensor('h_total', 0, 'h', data)
|
|
173
173
|
self.assertSensor('safety_country', 32, '', data)
|
|
174
174
|
self.assertSensor('safety_country_label', '50Hz 230Vac Default', '', data)
|
|
175
175
|
self.assertSensor('funbit', 512, '', data)
|
|
@@ -229,7 +229,7 @@ class GW5000D_NS_Test(DtMock):
|
|
|
229
229
|
self.assertSensor('funbit', 2400, '', data)
|
|
230
230
|
self.assertSensor('vbus', 291.7, 'V', data)
|
|
231
231
|
self.assertSensor('vnbus', 0, 'V', data)
|
|
232
|
-
self.assertSensor('derating_mode',
|
|
232
|
+
self.assertSensor('derating_mode', 0, '', data)
|
|
233
233
|
self.assertSensor('derating_mode_label', '', '', data)
|
|
234
234
|
|
|
235
235
|
def test_get_grid_export_limit(self):
|
|
@@ -295,7 +295,7 @@ class GW5000_MS_Test(DtMock):
|
|
|
295
295
|
self.assertSensor('funbit', 2384, '', data)
|
|
296
296
|
self.assertSensor('vbus', 393.9, 'V', data)
|
|
297
297
|
self.assertSensor('vnbus', 0, 'V', data)
|
|
298
|
-
self.assertSensor('derating_mode',
|
|
298
|
+
self.assertSensor('derating_mode', 0, '', data)
|
|
299
299
|
self.assertSensor('derating_mode_label', '', '', data)
|
|
300
300
|
|
|
301
301
|
|
|
@@ -369,7 +369,7 @@ class GW10K_ET_fw1023_Test(EtMock):
|
|
|
369
369
|
self.assertEqual('02041-23-S00', self.arm_firmware)
|
|
370
370
|
|
|
371
371
|
def test_GW10K_ET_setting_fw1023(self):
|
|
372
|
-
self.assertEqual(
|
|
372
|
+
self.assertEqual(46, len(self.settings()))
|
|
373
373
|
settings = {s.id_: s for s in self.settings()}
|
|
374
374
|
self.assertEqual('PeakShavingMode', type(settings.get("peak_shaving_mode")).__name__)
|
|
375
375
|
|
|
@@ -449,7 +449,7 @@ class GW6000_EH_Test(EtMock):
|
|
|
449
449
|
self.assertSensor('safety_country_label', 'ES-A', '', data)
|
|
450
450
|
self.assertSensor('work_mode', 1, '', data)
|
|
451
451
|
self.assertSensor('work_mode_label', 'Normal (On-Grid)', '', data)
|
|
452
|
-
self.assertSensor('operation_mode',
|
|
452
|
+
self.assertSensor('operation_mode', 0, '', data)
|
|
453
453
|
self.assertSensor('error_codes', 0, '', data)
|
|
454
454
|
self.assertSensor('errors', '', '', data)
|
|
455
455
|
self.assertSensor("e_total", 59.4, 'kWh', data)
|
|
@@ -550,7 +550,7 @@ class GEH10_1U_10_Test(EtMock):
|
|
|
550
550
|
self.assertSensor('safety_country_label', 'Australia A', '', data)
|
|
551
551
|
self.assertSensor('work_mode', 1, '', data)
|
|
552
552
|
self.assertSensor('work_mode_label', 'Normal (On-Grid)', '', data)
|
|
553
|
-
self.assertSensor('operation_mode',
|
|
553
|
+
self.assertSensor('operation_mode', 0, '', data)
|
|
554
554
|
self.assertSensor('error_codes', 0, '', data)
|
|
555
555
|
self.assertSensor('errors', '', '', data)
|
|
556
556
|
self.assertSensor('e_total', 10225.8, 'kWh', data)
|
|
@@ -1085,7 +1085,7 @@ class GW29K9_ET_Test(EtMock):
|
|
|
1085
1085
|
self.assertSensor('meter_apparent_power3', -3175, 'VA', data)
|
|
1086
1086
|
self.assertSensor('meter_apparent_power_total', -5667, 'VA', data)
|
|
1087
1087
|
self.assertSensor('meter_type', 2, '', data)
|
|
1088
|
-
self.assertSensor('meter_sw_version',
|
|
1088
|
+
self.assertSensor('meter_sw_version', 0, '', data)
|
|
1089
1089
|
self.assertSensor('meter2_active_power', 0, 'W', data)
|
|
1090
1090
|
self.assertSensor('meter2_e_total_exp', 0.0, 'kWh', data)
|
|
1091
1091
|
self.assertSensor('meter2_e_total_imp', 0.0, 'kWh', data)
|
|
@@ -52,6 +52,17 @@ class TestUtils(TestCase):
|
|
|
52
52
|
self.assertEqual(49, testee.read(data))
|
|
53
53
|
self.assertEqual("0031", testee.encode_value(49).hex())
|
|
54
54
|
|
|
55
|
+
data = MockResponse("ff9e")
|
|
56
|
+
self.assertEqual(65438, testee.read(data))
|
|
57
|
+
self.assertEqual("ff9e", testee.encode_value(65438).hex())
|
|
58
|
+
|
|
59
|
+
def test_integer_signed(self):
|
|
60
|
+
testee = IntegerS("", 0, "", "", None)
|
|
61
|
+
|
|
62
|
+
data = MockResponse("0031")
|
|
63
|
+
self.assertEqual(49, testee.read(data))
|
|
64
|
+
self.assertEqual("0031", testee.encode_value(49).hex())
|
|
65
|
+
|
|
55
66
|
data = MockResponse("ff9e")
|
|
56
67
|
self.assertEqual(-98, testee.read(data))
|
|
57
68
|
self.assertEqual("ff9e", testee.encode_value(-98).hex())
|
|
@@ -158,7 +169,6 @@ class TestUtils(TestCase):
|
|
|
158
169
|
data = MockResponse("0d1e0e28ffc4ff1a")
|
|
159
170
|
self.assertEqual("13:30-14:40 Mon,Wed,Thu -60% On", testee.read(data).__str__())
|
|
160
171
|
self.assertEqual(bytes.fromhex("0d1e0e28ffc4ff1a"), testee.encode_value(bytes.fromhex("0d1e0e28ffc4ff1a")))
|
|
161
|
-
self.assertRaises(ValueError, lambda: testee.encode_value(bytes.fromhex("0d1e0e28ffc4ffff")))
|
|
162
172
|
self.assertRaises(ValueError, lambda: testee.encode_value("some string"))
|
|
163
173
|
self.assertFalse(testee.read(data).is_eco_charge_mode())
|
|
164
174
|
self.assertFalse(testee.read(data).is_eco_discharge_mode())
|
|
@@ -188,7 +198,6 @@ class TestUtils(TestCase):
|
|
|
188
198
|
self.assertEqual(ScheduleType.ECO_MODE, testee.schedule_type)
|
|
189
199
|
self.assertEqual(bytes.fromhex("0d1e0e28ff1affc4005a0000"),
|
|
190
200
|
testee.encode_value(bytes.fromhex("0d1e0e28ff1affc4005a0000")))
|
|
191
|
-
self.assertRaises(ValueError, lambda: testee.encode_value(bytes.fromhex("0d1e0e28ffffffc4005a0000")))
|
|
192
201
|
self.assertRaises(ValueError, lambda: testee.encode_value("some string"))
|
|
193
202
|
self.assertFalse(testee.read(data).is_eco_charge_mode())
|
|
194
203
|
self.assertFalse(testee.read(data).is_eco_discharge_mode())
|
|
@@ -208,6 +217,8 @@ class TestUtils(TestCase):
|
|
|
208
217
|
self.assertFalse(testee.read(data).is_eco_charge_mode())
|
|
209
218
|
self.assertFalse(testee.read(data).is_eco_discharge_mode())
|
|
210
219
|
|
|
220
|
+
data = MockResponse("0300080006fefd12005fcfff")
|
|
221
|
+
self.assertEqual("3:0-8:0 Mon -75% (SoC 95%) Off", testee.read(data).__str__())
|
|
211
222
|
data = MockResponse("0000173b5500001400640000")
|
|
212
223
|
self.assertEqual("0:0-23:59 20% (SoC 100%) Unset", testee.read(data).__str__())
|
|
213
224
|
data = MockResponse("ffffffff557f000000010001")
|
goodwe-0.3.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.2
|
goodwe-0.3.2/goodwe/model.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
from .inverter import Inverter
|
|
2
|
-
|
|
3
|
-
# Serial number tags to identify inverter type
|
|
4
|
-
ET_MODEL_TAGS = ["ETU", "ETL", "ETR", "ETC", "EHU", "EHR", "EHB", "BTU", "BTN", "BTC", "BHU", "AES", "ABP", "HHI",
|
|
5
|
-
"HSB", "HUA", "CUA",
|
|
6
|
-
"ESN", "EMN", "ERN", "EBN", # ES Gen 2
|
|
7
|
-
"HLB", "HMB", "HBB", "SPN"] # Gen 2
|
|
8
|
-
ES_MODEL_TAGS = ["ESU", "EMU", "ESA", "BPS", "BPU", "EMJ", "IJL"]
|
|
9
|
-
DT_MODEL_TAGS = ["DTU", "DTS",
|
|
10
|
-
"MSU", "MST", "MSC", "DSN", "DTN", "DST", "NSU", "SSN", "SST", "SSX", "SSY",
|
|
11
|
-
"PSB", "PSC"]
|
|
12
|
-
|
|
13
|
-
SINGLE_PHASE_MODELS = ["DSN", "DST", "NSU", "SSN", "SST", "SSX", "SSY", # DT
|
|
14
|
-
"MSU", "MST", "PSB", "PSC",
|
|
15
|
-
"MSC", # Found on third gen MS
|
|
16
|
-
"EHU", "EHR", "HSB", # ET
|
|
17
|
-
"ESN", "EMN", "ERN", "EBN", "HLB", "HMB", "HBB", "SPN"] # ES Gen 2
|
|
18
|
-
|
|
19
|
-
MPPT3_MODELS = ["MSU", "MST", "PSC", "MSC",
|
|
20
|
-
"25KET", "29K9ET"]
|
|
21
|
-
|
|
22
|
-
MPPT4_MODELS = ["HSB"]
|
|
23
|
-
|
|
24
|
-
BAT_2_MODELS = ["25KET", "29K9ET"]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def is_single_phase(inverter: Inverter) -> bool:
|
|
28
|
-
return any(model in inverter.serial_number for model in SINGLE_PHASE_MODELS)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def is_3_mppt(inverter: Inverter) -> bool:
|
|
32
|
-
return any(model in inverter.serial_number for model in MPPT3_MODELS)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def is_4_mppt(inverter: Inverter) -> bool:
|
|
36
|
-
return any(model in inverter.serial_number for model in MPPT4_MODELS)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def is_2_battery(inverter: Inverter) -> bool:
|
|
40
|
-
return any(model in inverter.serial_number for model in BAT_2_MODELS)
|
|
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
|