goodwe 0.3.1__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.1/goodwe.egg-info → goodwe-0.3.3}/PKG-INFO +1 -1
- goodwe-0.3.3/VERSION +1 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/es.py +4 -4
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/et.py +44 -30
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/inverter.py +2 -2
- goodwe-0.3.3/goodwe/model.py +50 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/sensor.py +159 -44
- {goodwe-0.3.1 → goodwe-0.3.3/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.3.1 → goodwe-0.3.3}/tests/test_dt.py +13 -13
- {goodwe-0.3.1 → goodwe-0.3.3}/tests/test_et.py +7 -7
- {goodwe-0.3.1 → goodwe-0.3.3}/tests/test_sensor.py +46 -3
- goodwe-0.3.1/VERSION +0 -1
- goodwe-0.3.1/goodwe/model.py +0 -40
- {goodwe-0.3.1 → goodwe-0.3.3}/LICENSE +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/README.md +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/__init__.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/const.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/dt.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/exceptions.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/modbus.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe/protocol.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/pyproject.toml +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/setup.cfg +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/tests/test_es.py +0 -0
- {goodwe-0.3.1 → goodwe-0.3.3}/tests/test_modbus.py +0 -0
- {goodwe-0.3.1 → 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")
|
|
@@ -67,7 +67,7 @@ class ES(Inverter):
|
|
|
67
67
|
Voltage("vgrid", 34, "On-grid Voltage", Kind.AC),
|
|
68
68
|
Current("igrid", 36, "On-grid Current", Kind.AC),
|
|
69
69
|
Calculated("pgrid",
|
|
70
|
-
lambda data: abs(
|
|
70
|
+
lambda data: abs(read_bytes2_signed(data, 38)) * (-1 if read_byte(data, 80) == 2 else 1),
|
|
71
71
|
"On-grid Export Power", "W", Kind.AC),
|
|
72
72
|
Frequency("fgrid", 40, "On-grid Frequency", Kind.AC),
|
|
73
73
|
Byte("grid_mode", 42, "Work Mode code", "", Kind.GRID),
|
|
@@ -87,7 +87,7 @@ class ES(Inverter):
|
|
|
87
87
|
Energy("e_day", 67, "Today's PV Generation", Kind.PV),
|
|
88
88
|
Energy("e_load_day", 69, "Today's Load", Kind.AC),
|
|
89
89
|
Energy4("e_load_total", 71, "Total Load", Kind.AC),
|
|
90
|
-
|
|
90
|
+
PowerS("total_power", 75, "Total Power", Kind.AC), # modbus 0x52c
|
|
91
91
|
Byte("effective_work_mode", 77, "Effective Work Mode code"),
|
|
92
92
|
Integer("effective_relay_control", 78, "Effective Relay Control", "", None),
|
|
93
93
|
Byte("grid_in_out", 80, "On-grid Mode code", "", Kind.GRID),
|
|
@@ -121,7 +121,7 @@ class ES(Inverter):
|
|
|
121
121
|
round(read_voltage(data, 5) * read_current(data, 7)) +
|
|
122
122
|
(abs(round(read_voltage(data, 10) * read_current(data, 18))) *
|
|
123
123
|
(-1 if read_byte(data, 30) == 3 else 1)) -
|
|
124
|
-
(abs(
|
|
124
|
+
(abs(read_bytes2_signed(data, 38)) * (-1 if read_byte(data, 80) == 2 else 1)),
|
|
125
125
|
"House Consumption", "W", Kind.AC),
|
|
126
126
|
)
|
|
127
127
|
|
|
@@ -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, ...] = (
|
|
@@ -52,23 +52,23 @@ class ET(Inverter):
|
|
|
52
52
|
Current("igrid", 35122, "On-grid L1 Current", Kind.AC),
|
|
53
53
|
Frequency("fgrid", 35123, "On-grid L1 Frequency", Kind.AC),
|
|
54
54
|
# 35124 reserved
|
|
55
|
-
|
|
55
|
+
PowerS("pgrid", 35125, "On-grid L1 Power", Kind.AC),
|
|
56
56
|
Voltage("vgrid2", 35126, "On-grid L2 Voltage", Kind.AC),
|
|
57
57
|
Current("igrid2", 35127, "On-grid L2 Current", Kind.AC),
|
|
58
58
|
Frequency("fgrid2", 35128, "On-grid L2 Frequency", Kind.AC),
|
|
59
59
|
# 35129 reserved
|
|
60
|
-
|
|
60
|
+
PowerS("pgrid2", 35130, "On-grid L2 Power", Kind.AC),
|
|
61
61
|
Voltage("vgrid3", 35131, "On-grid L3 Voltage", Kind.AC),
|
|
62
62
|
Current("igrid3", 35132, "On-grid L3 Current", Kind.AC),
|
|
63
63
|
Frequency("fgrid3", 35133, "On-grid L3 Frequency", Kind.AC),
|
|
64
64
|
# 35134 reserved
|
|
65
|
-
|
|
65
|
+
PowerS("pgrid3", 35135, "On-grid L3 Power", Kind.AC),
|
|
66
66
|
Integer("grid_mode", 35136, "Grid Mode code", "", Kind.PV),
|
|
67
67
|
Enum2("grid_mode_label", 35136, GRID_MODES, "Grid Mode", Kind.PV),
|
|
68
68
|
# 35137 reserved
|
|
69
|
-
|
|
69
|
+
PowerS("total_inverter_power", 35138, "Total Power", Kind.AC),
|
|
70
70
|
# 35139 reserved
|
|
71
|
-
|
|
71
|
+
PowerS("active_power", 35140, "Active Power", Kind.GRID),
|
|
72
72
|
Calculated("grid_in_out",
|
|
73
73
|
lambda data: read_grid_mode(data, 35140),
|
|
74
74
|
"On-grid Mode code", "", Kind.GRID),
|
|
@@ -84,29 +84,29 @@ class ET(Inverter):
|
|
|
84
84
|
Frequency("backup_f1", 35147, "Back-up L1 Frequency", Kind.UPS),
|
|
85
85
|
Integer("load_mode1", 35148, "Load Mode L1"),
|
|
86
86
|
# 35149 reserved
|
|
87
|
-
|
|
87
|
+
PowerS("backup_p1", 35150, "Back-up L1 Power", Kind.UPS),
|
|
88
88
|
Voltage("backup_v2", 35151, "Back-up L2 Voltage", Kind.UPS),
|
|
89
89
|
Current("backup_i2", 35152, "Back-up L2 Current", Kind.UPS),
|
|
90
90
|
Frequency("backup_f2", 35153, "Back-up L2 Frequency", Kind.UPS),
|
|
91
91
|
Integer("load_mode2", 35154, "Load Mode L2"),
|
|
92
92
|
# 35155 reserved
|
|
93
|
-
|
|
93
|
+
PowerS("backup_p2", 35156, "Back-up L2 Power", Kind.UPS),
|
|
94
94
|
Voltage("backup_v3", 35157, "Back-up L3 Voltage", Kind.UPS),
|
|
95
95
|
Current("backup_i3", 35158, "Back-up L3 Current", Kind.UPS),
|
|
96
96
|
Frequency("backup_f3", 35159, "Back-up L3 Frequency", Kind.UPS),
|
|
97
97
|
Integer("load_mode3", 35160, "Load Mode L3"),
|
|
98
98
|
# 35161 reserved
|
|
99
|
-
|
|
99
|
+
PowerS("backup_p3", 35162, "Back-up L3 Power", Kind.UPS),
|
|
100
100
|
# 35163 reserved
|
|
101
|
-
|
|
101
|
+
PowerS("load_p1", 35164, "Load L1", Kind.AC),
|
|
102
102
|
# 35165 reserved
|
|
103
|
-
|
|
103
|
+
PowerS("load_p2", 35166, "Load L2", Kind.AC),
|
|
104
104
|
# 35167 reserved
|
|
105
|
-
|
|
105
|
+
PowerS("load_p3", 35168, "Load L3", Kind.AC),
|
|
106
106
|
# 35169 reserved
|
|
107
|
-
|
|
107
|
+
PowerS("backup_ptotal", 35170, "Back-up Load", Kind.UPS),
|
|
108
108
|
# 35171 reserved
|
|
109
|
-
|
|
109
|
+
PowerS("load_ptotal", 35172, "Load", Kind.AC),
|
|
110
110
|
Integer("ups_load", 35173, "Ups Load", "%", Kind.UPS),
|
|
111
111
|
Temp("temperature_air", 35174, "Inverter Temperature (Air)", Kind.AC),
|
|
112
112
|
Temp("temperature_module", 35175, "Inverter Temperature (Module)"),
|
|
@@ -115,8 +115,8 @@ class ET(Inverter):
|
|
|
115
115
|
Voltage("bus_voltage", 35178, "Bus Voltage", None),
|
|
116
116
|
Voltage("nbus_voltage", 35179, "NBus Voltage", None),
|
|
117
117
|
Voltage("vbattery1", 35180, "Battery Voltage", Kind.BAT),
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
CurrentS("ibattery1", 35181, "Battery Current", Kind.BAT),
|
|
119
|
+
Power4S("pbattery1", 35182, "Battery Power", Kind.BAT),
|
|
120
120
|
Integer("battery_mode", 35184, "Battery Mode code", "", Kind.BAT),
|
|
121
121
|
Enum2("battery_mode_label", 35184, BATTERY_MODES, "Battery Mode", Kind.BAT),
|
|
122
122
|
Integer("warning_code", 35185, "Warning code"),
|
|
@@ -149,8 +149,8 @@ class ET(Inverter):
|
|
|
149
149
|
read_bytes4(data, 35109) +
|
|
150
150
|
read_bytes4(data, 35113) +
|
|
151
151
|
read_bytes4(data, 35117) +
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
read_bytes4_signed(data, 35182) -
|
|
153
|
+
read_bytes2_signed(data, 35140),
|
|
154
154
|
"House Consumption", "W", Kind.AC),
|
|
155
155
|
)
|
|
156
156
|
|
|
@@ -226,10 +226,10 @@ class ET(Inverter):
|
|
|
226
226
|
Integer("manufacture_code", 36002, "Manufacture Code"),
|
|
227
227
|
Integer("meter_test_status", 36003, "Meter Test Status"), # 1: correct,2: reverse,3: incorrect,0: not checked
|
|
228
228
|
Integer("meter_comm_status", 36004, "Meter Communication Status"), # 1 OK, 0 NotOK
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
PowerS("active_power1", 36005, "Active Power L1", Kind.GRID),
|
|
230
|
+
PowerS("active_power2", 36006, "Active Power L2", Kind.GRID),
|
|
231
|
+
PowerS("active_power3", 36007, "Active Power L3", Kind.GRID),
|
|
232
|
+
PowerS("active_power_total", 36008, "Active Power Total", Kind.GRID),
|
|
233
233
|
Reactive("reactive_power_total", 36009, "Reactive Power Total", Kind.GRID),
|
|
234
234
|
Decimal("meter_power_factor1", 36010, 1000, "Meter Power Factor L1", "", Kind.GRID),
|
|
235
235
|
Decimal("meter_power_factor2", 36011, 1000, "Meter Power Factor L2", "", Kind.GRID),
|
|
@@ -238,10 +238,10 @@ class ET(Inverter):
|
|
|
238
238
|
Frequency("meter_freq", 36014, "Meter Frequency", Kind.GRID),
|
|
239
239
|
Float("meter_e_total_exp", 36015, 1000, "Meter Total Energy (export)", "kWh", Kind.GRID),
|
|
240
240
|
Float("meter_e_total_imp", 36017, 1000, "Meter Total Energy (import)", "kWh", Kind.GRID),
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
Power4S("meter_active_power1", 36019, "Meter Active Power L1", Kind.GRID),
|
|
242
|
+
Power4S("meter_active_power2", 36021, "Meter Active Power L2", Kind.GRID),
|
|
243
|
+
Power4S("meter_active_power3", 36023, "Meter Active Power L3", Kind.GRID),
|
|
244
|
+
Power4S("meter_active_power_total", 36025, "Meter Active Power Total", Kind.GRID),
|
|
245
245
|
Reactive4("meter_reactive_power1", 36027, "Meter Reactive Power L1", Kind.GRID),
|
|
246
246
|
Reactive4("meter_reactive_power2", 36029, "Meter Reactive Power L2", Kind.GRID),
|
|
247
247
|
Reactive4("meter_reactive_power3", 36031, "Meter Reactive Power L2", Kind.GRID),
|
|
@@ -253,7 +253,7 @@ class ET(Inverter):
|
|
|
253
253
|
Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
|
|
254
254
|
Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
|
|
255
255
|
# Sensors added in some ARM fw update, read when flag _has_meter_extended is on
|
|
256
|
-
|
|
256
|
+
Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
|
|
257
257
|
Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
|
|
258
258
|
Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
|
|
259
259
|
Integer("meter2_comm_status", 36051, "Meter 2 Communication Status"),
|
|
@@ -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,14 +80,14 @@ 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
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
class Voltage(Sensor):
|
|
86
|
-
"""Sensor representing voltage [V] value encoded in 2 bytes"""
|
|
90
|
+
"""Sensor representing voltage [V] value encoded in 2 (unsigned) bytes"""
|
|
87
91
|
|
|
88
92
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
89
93
|
super().__init__(id_, offset, name, 2, "V", kind)
|
|
@@ -96,7 +100,7 @@ class Voltage(Sensor):
|
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
class Current(Sensor):
|
|
99
|
-
"""Sensor representing current [A] value encoded in 2 bytes"""
|
|
103
|
+
"""Sensor representing current [A] value encoded in 2 (unsigned) bytes"""
|
|
100
104
|
|
|
101
105
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
102
106
|
super().__init__(id_, offset, name, 2, "A", kind)
|
|
@@ -108,6 +112,19 @@ class Current(Sensor):
|
|
|
108
112
|
return encode_current(value)
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
class CurrentS(Sensor):
|
|
116
|
+
"""Sensor representing current [A] value encoded in 2 (signed) bytes"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
119
|
+
super().__init__(id_, offset, name, 2, "A", kind)
|
|
120
|
+
|
|
121
|
+
def read_value(self, data: ProtocolResponse):
|
|
122
|
+
return read_current_signed(data)
|
|
123
|
+
|
|
124
|
+
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
125
|
+
return encode_current_signed(value)
|
|
126
|
+
|
|
127
|
+
|
|
111
128
|
class Frequency(Sensor):
|
|
112
129
|
"""Sensor representing frequency [Hz] value encoded in 2 bytes"""
|
|
113
130
|
|
|
@@ -119,7 +136,7 @@ class Frequency(Sensor):
|
|
|
119
136
|
|
|
120
137
|
|
|
121
138
|
class Power(Sensor):
|
|
122
|
-
"""Sensor representing power [W] value encoded in 2 bytes"""
|
|
139
|
+
"""Sensor representing power [W] value encoded in 2 (unsigned) bytes"""
|
|
123
140
|
|
|
124
141
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
125
142
|
super().__init__(id_, offset, name, 2, "W", kind)
|
|
@@ -128,8 +145,18 @@ class Power(Sensor):
|
|
|
128
145
|
return read_bytes2(data)
|
|
129
146
|
|
|
130
147
|
|
|
148
|
+
class PowerS(Sensor):
|
|
149
|
+
"""Sensor representing power [W] value encoded in 2 (signed) bytes"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
152
|
+
super().__init__(id_, offset, name, 2, "W", kind)
|
|
153
|
+
|
|
154
|
+
def read_value(self, data: ProtocolResponse):
|
|
155
|
+
return read_bytes2_signed(data)
|
|
156
|
+
|
|
157
|
+
|
|
131
158
|
class Power4(Sensor):
|
|
132
|
-
"""Sensor representing power [W] value encoded in 4 bytes"""
|
|
159
|
+
"""Sensor representing power [W] value encoded in 4 (unsigned) bytes"""
|
|
133
160
|
|
|
134
161
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
135
162
|
super().__init__(id_, offset, name, 4, "W", kind)
|
|
@@ -138,6 +165,16 @@ class Power4(Sensor):
|
|
|
138
165
|
return read_bytes4(data)
|
|
139
166
|
|
|
140
167
|
|
|
168
|
+
class Power4S(Sensor):
|
|
169
|
+
"""Sensor representing power [W] value encoded in 4 (signed) bytes"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
172
|
+
super().__init__(id_, offset, name, 4, "W", kind)
|
|
173
|
+
|
|
174
|
+
def read_value(self, data: ProtocolResponse):
|
|
175
|
+
return read_bytes4_signed(data)
|
|
176
|
+
|
|
177
|
+
|
|
141
178
|
class Energy(Sensor):
|
|
142
179
|
"""Sensor representing energy [kWh] value encoded in 2 bytes"""
|
|
143
180
|
|
|
@@ -146,10 +183,7 @@ class Energy(Sensor):
|
|
|
146
183
|
|
|
147
184
|
def read_value(self, data: ProtocolResponse):
|
|
148
185
|
value = read_bytes2(data)
|
|
149
|
-
|
|
150
|
-
return None
|
|
151
|
-
else:
|
|
152
|
-
return float(value) / 10
|
|
186
|
+
return float(value) / 10
|
|
153
187
|
|
|
154
188
|
|
|
155
189
|
class Energy4(Sensor):
|
|
@@ -160,10 +194,7 @@ class Energy4(Sensor):
|
|
|
160
194
|
|
|
161
195
|
def read_value(self, data: ProtocolResponse):
|
|
162
196
|
value = read_bytes4(data)
|
|
163
|
-
|
|
164
|
-
return None
|
|
165
|
-
else:
|
|
166
|
-
return float(value) / 10
|
|
197
|
+
return float(value) / 10
|
|
167
198
|
|
|
168
199
|
|
|
169
200
|
class Apparent(Sensor):
|
|
@@ -173,7 +204,7 @@ class Apparent(Sensor):
|
|
|
173
204
|
super().__init__(id_, offset, name, 2, "VA", kind)
|
|
174
205
|
|
|
175
206
|
def read_value(self, data: ProtocolResponse):
|
|
176
|
-
return
|
|
207
|
+
return read_bytes2_signed(data)
|
|
177
208
|
|
|
178
209
|
|
|
179
210
|
class Apparent4(Sensor):
|
|
@@ -183,7 +214,7 @@ class Apparent4(Sensor):
|
|
|
183
214
|
super().__init__(id_, offset, name, 2, "VA", kind)
|
|
184
215
|
|
|
185
216
|
def read_value(self, data: ProtocolResponse):
|
|
186
|
-
return
|
|
217
|
+
return read_bytes4_signed(data)
|
|
187
218
|
|
|
188
219
|
|
|
189
220
|
class Reactive(Sensor):
|
|
@@ -193,7 +224,7 @@ class Reactive(Sensor):
|
|
|
193
224
|
super().__init__(id_, offset, name, 2, "var", kind)
|
|
194
225
|
|
|
195
226
|
def read_value(self, data: ProtocolResponse):
|
|
196
|
-
return
|
|
227
|
+
return read_bytes2_signed(data)
|
|
197
228
|
|
|
198
229
|
|
|
199
230
|
class Reactive4(Sensor):
|
|
@@ -203,7 +234,7 @@ class Reactive4(Sensor):
|
|
|
203
234
|
super().__init__(id_, offset, name, 2, "var", kind)
|
|
204
235
|
|
|
205
236
|
def read_value(self, data: ProtocolResponse):
|
|
206
|
-
return
|
|
237
|
+
return read_bytes4_signed(data)
|
|
207
238
|
|
|
208
239
|
|
|
209
240
|
class Temp(Sensor):
|
|
@@ -271,7 +302,7 @@ class ByteL(Byte):
|
|
|
271
302
|
|
|
272
303
|
|
|
273
304
|
class Integer(Sensor):
|
|
274
|
-
"""Sensor representing
|
|
305
|
+
"""Sensor representing unsigned int value encoded in 2 bytes"""
|
|
275
306
|
|
|
276
307
|
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
277
308
|
super().__init__(id_, offset, name, 2, unit, kind)
|
|
@@ -279,12 +310,25 @@ class Integer(Sensor):
|
|
|
279
310
|
def read_value(self, data: ProtocolResponse):
|
|
280
311
|
return read_bytes2(data)
|
|
281
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):
|
|
318
|
+
"""Sensor representing signed int value encoded in 2 bytes"""
|
|
319
|
+
|
|
320
|
+
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
321
|
+
super().__init__(id_, offset, name, 2, unit, kind)
|
|
322
|
+
|
|
323
|
+
def read_value(self, data: ProtocolResponse):
|
|
324
|
+
return read_bytes2_signed(data)
|
|
325
|
+
|
|
282
326
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
283
327
|
return int.to_bytes(int(value), length=2, byteorder="big", signed=True)
|
|
284
328
|
|
|
285
329
|
|
|
286
330
|
class Long(Sensor):
|
|
287
|
-
"""Sensor representing
|
|
331
|
+
"""Sensor representing unsigned int value encoded in 4 bytes"""
|
|
288
332
|
|
|
289
333
|
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
290
334
|
super().__init__(id_, offset, name, 4, unit, kind)
|
|
@@ -292,6 +336,19 @@ class Long(Sensor):
|
|
|
292
336
|
def read_value(self, data: ProtocolResponse):
|
|
293
337
|
return read_bytes4(data)
|
|
294
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):
|
|
344
|
+
"""Sensor representing signed int value encoded in 4 bytes"""
|
|
345
|
+
|
|
346
|
+
def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None):
|
|
347
|
+
super().__init__(id_, offset, name, 4, unit, kind)
|
|
348
|
+
|
|
349
|
+
def read_value(self, data: ProtocolResponse):
|
|
350
|
+
return read_bytes4_signed(data)
|
|
351
|
+
|
|
295
352
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
296
353
|
return int.to_bytes(int(value), length=4, byteorder="big", signed=True)
|
|
297
354
|
|
|
@@ -390,7 +447,7 @@ class EnumBitmap4(Sensor):
|
|
|
390
447
|
raise NotImplementedError()
|
|
391
448
|
|
|
392
449
|
def read(self, data: ProtocolResponse):
|
|
393
|
-
bits =
|
|
450
|
+
bits = read_bytes4_signed(data, self.offset)
|
|
394
451
|
return decode_bitmap(bits if bits != -1 else 0, self._labels)
|
|
395
452
|
|
|
396
453
|
|
|
@@ -449,6 +506,14 @@ class EcoMode(ABC):
|
|
|
449
506
|
def is_eco_discharge_mode(self) -> bool:
|
|
450
507
|
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
|
|
451
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
|
+
|
|
452
517
|
|
|
453
518
|
class EcoModeV1(Sensor, EcoMode):
|
|
454
519
|
"""Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
|
|
@@ -483,7 +548,7 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
483
548
|
self.end_m = read_byte(data)
|
|
484
549
|
if self.end_m < 0 or self.end_m > 59:
|
|
485
550
|
raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.")
|
|
486
|
-
self.power =
|
|
551
|
+
self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
|
|
487
552
|
if self.power < -100 or self.power > 100:
|
|
488
553
|
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
|
|
489
554
|
self.on_off = read_byte(data)
|
|
@@ -491,8 +556,6 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
491
556
|
raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
|
|
492
557
|
self.day_bits = read_byte(data)
|
|
493
558
|
self.days = decode_day_of_week(self.day_bits)
|
|
494
|
-
if self.day_bits < 0:
|
|
495
|
-
raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
|
|
496
559
|
return self
|
|
497
560
|
|
|
498
561
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
@@ -534,6 +597,14 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
534
597
|
and self.day_bits == 127 \
|
|
535
598
|
and self.power > 0
|
|
536
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
|
+
|
|
537
608
|
def as_eco_mode_v2(self) -> EcoModeV2:
|
|
538
609
|
"""Convert V1 to V2 EcoMode"""
|
|
539
610
|
result = EcoModeV2(self.id_, self.offset, self.name)
|
|
@@ -590,15 +661,13 @@ class Schedule(Sensor, EcoMode):
|
|
|
590
661
|
self.schedule_type = ScheduleType.detect_schedule_type(self.on_off)
|
|
591
662
|
self.day_bits = read_byte(data)
|
|
592
663
|
self.days = decode_day_of_week(self.day_bits)
|
|
593
|
-
|
|
594
|
-
raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
|
|
595
|
-
self.power = read_bytes2(data) # negative=charge, positive=discharge
|
|
664
|
+
self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
|
|
596
665
|
if not self.schedule_type.is_in_range(self.power):
|
|
597
666
|
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
|
|
598
|
-
self.soc =
|
|
667
|
+
self.soc = read_bytes2_signed(data)
|
|
599
668
|
if self.soc < 0 or self.soc > 100:
|
|
600
669
|
raise ValueError(f"{self.id_}: SoC value {self.soc} out of range.")
|
|
601
|
-
self.month_bits =
|
|
670
|
+
self.month_bits = read_bytes2_signed(data)
|
|
602
671
|
self.months = decode_months(self.month_bits)
|
|
603
672
|
return self
|
|
604
673
|
|
|
@@ -653,6 +722,19 @@ class Schedule(Sensor, EcoMode):
|
|
|
653
722
|
and self.power > 0 \
|
|
654
723
|
and (self.month_bits == 0 or self.month_bits == 0x0fff)
|
|
655
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
|
+
|
|
656
738
|
def as_eco_mode_v1(self) -> EcoModeV1:
|
|
657
739
|
"""Convert V2 to V1 EcoMode"""
|
|
658
740
|
result = EcoModeV1(self.id_, self.offset, self.name)
|
|
@@ -704,6 +786,14 @@ def read_byte(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
704
786
|
|
|
705
787
|
|
|
706
788
|
def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
789
|
+
"""Retrieve 2 byte (unsigned int) value from buffer"""
|
|
790
|
+
if offset is not None:
|
|
791
|
+
buffer.seek(offset)
|
|
792
|
+
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
793
|
+
return value if value != 0xffff else 0
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def read_bytes2_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
707
797
|
"""Retrieve 2 byte (signed int) value from buffer"""
|
|
708
798
|
if offset is not None:
|
|
709
799
|
buffer.seek(offset)
|
|
@@ -711,6 +801,14 @@ def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
711
801
|
|
|
712
802
|
|
|
713
803
|
def read_bytes4(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
804
|
+
"""Retrieve 4 byte (unsigned int) value from buffer"""
|
|
805
|
+
if offset is not None:
|
|
806
|
+
buffer.seek(offset)
|
|
807
|
+
value = int.from_bytes(buffer.read(4), byteorder="big", signed=False)
|
|
808
|
+
return value if value != 0xffffffff else 0
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
714
812
|
"""Retrieve 4 byte (signed int) value from buffer"""
|
|
715
813
|
if offset is not None:
|
|
716
814
|
buffer.seek(offset)
|
|
@@ -736,20 +834,28 @@ def read_float4(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
|
736
834
|
|
|
737
835
|
|
|
738
836
|
def read_voltage(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
739
|
-
"""Retrieve voltage [V] value (2 bytes) from buffer"""
|
|
837
|
+
"""Retrieve voltage [V] value (2 unsigned bytes) from buffer"""
|
|
740
838
|
if offset is not None:
|
|
741
839
|
buffer.seek(offset)
|
|
742
|
-
value = int.from_bytes(buffer.read(2), byteorder="big", signed=
|
|
743
|
-
return float(value) / 10
|
|
840
|
+
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
841
|
+
return float(value) / 10 if value != 0xffff else 0
|
|
744
842
|
|
|
745
843
|
|
|
746
844
|
def encode_voltage(value: Any) -> bytes:
|
|
747
|
-
"""Encode voltage value to raw (2 bytes) payload"""
|
|
748
|
-
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=
|
|
845
|
+
"""Encode voltage value to raw (2 unsigned bytes) payload"""
|
|
846
|
+
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=False)
|
|
749
847
|
|
|
750
848
|
|
|
751
849
|
def read_current(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
752
|
-
"""Retrieve current [A] value (2 bytes) from buffer"""
|
|
850
|
+
"""Retrieve current [A] value (2 unsigned bytes) from buffer"""
|
|
851
|
+
if offset is not None:
|
|
852
|
+
buffer.seek(offset)
|
|
853
|
+
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
854
|
+
return float(value) / 10 if value != 0xffff else 0
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def read_current_signed(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
858
|
+
"""Retrieve current [A] value (2 signed bytes) from buffer"""
|
|
753
859
|
if offset is not None:
|
|
754
860
|
buffer.seek(offset)
|
|
755
861
|
value = int.from_bytes(buffer.read(2), byteorder="big", signed=True)
|
|
@@ -757,7 +863,12 @@ def read_current(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
|
757
863
|
|
|
758
864
|
|
|
759
865
|
def encode_current(value: Any) -> bytes:
|
|
760
|
-
"""Encode current value to raw (2 bytes) payload"""
|
|
866
|
+
"""Encode current value to raw (2 unsigned bytes) payload"""
|
|
867
|
+
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=False)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def encode_current_signed(value: Any) -> bytes:
|
|
871
|
+
"""Encode current value to raw (2 signed bytes) payload"""
|
|
761
872
|
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=True)
|
|
762
873
|
|
|
763
874
|
|
|
@@ -809,7 +920,7 @@ def encode_datetime(value: Any) -> bytes:
|
|
|
809
920
|
|
|
810
921
|
def read_grid_mode(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
811
922
|
"""Retrieve 'grid mode' sign value from buffer"""
|
|
812
|
-
value =
|
|
923
|
+
value = read_bytes2_signed(buffer, offset)
|
|
813
924
|
if value < -90:
|
|
814
925
|
return 2
|
|
815
926
|
elif value >= 90:
|
|
@@ -835,6 +946,10 @@ def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
|
|
|
835
946
|
|
|
836
947
|
|
|
837
948
|
def decode_day_of_week(data: int) -> str:
|
|
949
|
+
if data == -1:
|
|
950
|
+
return "Mon-Sun"
|
|
951
|
+
elif data == 0:
|
|
952
|
+
return ""
|
|
838
953
|
bits = bin(data)[2:]
|
|
839
954
|
daynames = list(DAY_NAMES)
|
|
840
955
|
days = ""
|
|
@@ -848,7 +963,7 @@ def decode_day_of_week(data: int) -> str:
|
|
|
848
963
|
|
|
849
964
|
|
|
850
965
|
def decode_months(data: int) -> str | None:
|
|
851
|
-
if data
|
|
966
|
+
if data <= 0 or data == 0x0fff:
|
|
852
967
|
return None
|
|
853
968
|
bits = bin(data)[2:]
|
|
854
969
|
monthnames = list(MONTH_NAMES)
|
|
@@ -64,9 +64,9 @@ class GW6000_DT_Test(DtMock):
|
|
|
64
64
|
self.assertSensor('vpv3', None, 'V', data)
|
|
65
65
|
self.assertSensor('ipv3', None, 'A', data)
|
|
66
66
|
self.assertSensor('ppv3', None, 'W', data)
|
|
67
|
-
self.assertSensor('vline1',
|
|
68
|
-
self.assertSensor('vline2',
|
|
69
|
-
self.assertSensor('vline3',
|
|
67
|
+
self.assertSensor('vline1', 0, 'V', data)
|
|
68
|
+
self.assertSensor('vline2', 0, 'V', data)
|
|
69
|
+
self.assertSensor('vline3', 0, 'V', data)
|
|
70
70
|
self.assertSensor('vgrid1', 225.6, 'V', data)
|
|
71
71
|
self.assertSensor('vgrid2', 229.7, 'V', data)
|
|
72
72
|
self.assertSensor('vgrid3', 231.0, 'V', data)
|
|
@@ -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}")
|
|
@@ -167,9 +167,9 @@ class GW8K_DT_Test(DtMock):
|
|
|
167
167
|
self.assertSensor("apparent_power", 0, "VA", data),
|
|
168
168
|
self.assertSensor("reactive_power", 0, "var", data),
|
|
169
169
|
self.assertSensor('temperature', 45.3, 'C', data)
|
|
170
|
-
self.assertSensor('e_day',
|
|
171
|
-
self.assertSensor('e_total',
|
|
172
|
-
self.assertSensor('h_total',
|
|
170
|
+
self.assertSensor('e_day', 0.0, 'kWh', data)
|
|
171
|
+
self.assertSensor('e_total', 0.0, 'kWh', data)
|
|
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)
|
|
@@ -208,7 +208,7 @@ class GW5000D_NS_Test(DtMock):
|
|
|
208
208
|
self.assertSensor('vpv2', 291.8, 'V', data)
|
|
209
209
|
self.assertSensor('ipv2', 0, 'A', data)
|
|
210
210
|
self.assertSensor('ppv2', 0, 'W', data)
|
|
211
|
-
self.assertSensor('vline1',
|
|
211
|
+
self.assertSensor('vline1', 0, 'V', data)
|
|
212
212
|
self.assertSensor('vgrid1', 240.5, 'V', data)
|
|
213
213
|
self.assertSensor('igrid1', 0.0, 'A', data)
|
|
214
214
|
self.assertSensor('fgrid1', 49.97, 'Hz', data)
|
|
@@ -228,8 +228,8 @@ class GW5000D_NS_Test(DtMock):
|
|
|
228
228
|
self.assertSensor('safety_country_label', 'Australia Victoria', '', data)
|
|
229
229
|
self.assertSensor('funbit', 2400, '', data)
|
|
230
230
|
self.assertSensor('vbus', 291.7, 'V', data)
|
|
231
|
-
self.assertSensor('vnbus',
|
|
232
|
-
self.assertSensor('derating_mode',
|
|
231
|
+
self.assertSensor('vnbus', 0, 'V', data)
|
|
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):
|
|
@@ -274,7 +274,7 @@ class GW5000_MS_Test(DtMock):
|
|
|
274
274
|
self.assertSensor('vpv3', 143.2, 'V', data)
|
|
275
275
|
self.assertSensor('ipv3', 0.4, 'A', data)
|
|
276
276
|
self.assertSensor('ppv3', 57, 'W', data)
|
|
277
|
-
self.assertSensor('vline1',
|
|
277
|
+
self.assertSensor('vline1', 0, 'V', data)
|
|
278
278
|
self.assertSensor('vgrid1', 240.1, 'V', data)
|
|
279
279
|
self.assertSensor('igrid1', 0.9, 'A', data)
|
|
280
280
|
self.assertSensor('fgrid1', 49.98, 'Hz', data)
|
|
@@ -294,8 +294,8 @@ class GW5000_MS_Test(DtMock):
|
|
|
294
294
|
self.assertSensor('safety_country_label', 'Australia Victoria', '', data)
|
|
295
295
|
self.assertSensor('funbit', 2384, '', data)
|
|
296
296
|
self.assertSensor('vbus', 393.9, 'V', data)
|
|
297
|
-
self.assertSensor('vnbus',
|
|
298
|
-
self.assertSensor('derating_mode',
|
|
297
|
+
self.assertSensor('vnbus', 0, 'V', data)
|
|
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
|
|
|
@@ -438,7 +438,7 @@ class GW6000_EH_Test(EtMock):
|
|
|
438
438
|
self.assertSensor('temperature', 38.6, 'C', data)
|
|
439
439
|
self.assertSensor('function_bit', 256, '', data)
|
|
440
440
|
self.assertSensor('bus_voltage', 380.6, 'V', data)
|
|
441
|
-
self.assertSensor('nbus_voltage',
|
|
441
|
+
self.assertSensor('nbus_voltage', 0, 'V', data)
|
|
442
442
|
self.assertSensor('vbattery1', 0.0, 'V', data)
|
|
443
443
|
self.assertSensor('ibattery1', 0.1, 'A', data)
|
|
444
444
|
self.assertSensor('pbattery1', 0, 'W', data)
|
|
@@ -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)
|
|
@@ -469,7 +469,7 @@ class GW6000_EH_Test(EtMock):
|
|
|
469
469
|
self.assertSensor('diagnose_result_label',
|
|
470
470
|
'Battery voltage low, Battery SOC low, Battery SOC in back, Discharge Driver On, Self-use load light, Battery Disconnected, Self-use off, Export power limit set, PF value set, Real power limit set',
|
|
471
471
|
'', data)
|
|
472
|
-
self.assertSensor('house_consumption',
|
|
472
|
+
self.assertSensor('house_consumption', 1712, 'W', data)
|
|
473
473
|
|
|
474
474
|
|
|
475
475
|
class GEH10_1U_10_Test(EtMock):
|
|
@@ -539,7 +539,7 @@ class GEH10_1U_10_Test(EtMock):
|
|
|
539
539
|
self.assertSensor('temperature', 67.0, 'C', data)
|
|
540
540
|
self.assertSensor('function_bit', 257, '', data)
|
|
541
541
|
self.assertSensor('bus_voltage', 458.4, 'V', data)
|
|
542
|
-
self.assertSensor('nbus_voltage',
|
|
542
|
+
self.assertSensor('nbus_voltage', 0, 'V', data)
|
|
543
543
|
self.assertSensor('vbattery1', 406.1, 'V', data)
|
|
544
544
|
self.assertSensor('ibattery1', -3.8, 'A', data)
|
|
545
545
|
self.assertSensor('pbattery1', -1566, 'W', 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())
|
|
@@ -78,6 +89,12 @@ class TestUtils(TestCase):
|
|
|
78
89
|
self.assertEqual(803.6, testee.read(data))
|
|
79
90
|
self.assertEqual("1f64", testee.encode_value(803.6).hex())
|
|
80
91
|
|
|
92
|
+
data = MockResponse("a000")
|
|
93
|
+
self.assertEqual(4096.0, testee.read(data))
|
|
94
|
+
|
|
95
|
+
data = MockResponse("ffff")
|
|
96
|
+
self.assertEqual(0, testee.read(data))
|
|
97
|
+
|
|
81
98
|
def test_current(self):
|
|
82
99
|
testee = Current("", 0, "", None)
|
|
83
100
|
|
|
@@ -85,6 +102,20 @@ class TestUtils(TestCase):
|
|
|
85
102
|
self.assertEqual(4.9, testee.read(data))
|
|
86
103
|
self.assertEqual("0031", testee.encode_value(4.9).hex())
|
|
87
104
|
|
|
105
|
+
data = MockResponse("ff9e")
|
|
106
|
+
self.assertEqual(6543.8, testee.read(data))
|
|
107
|
+
self.assertEqual("ff9e", testee.encode_value(6543.8).hex())
|
|
108
|
+
|
|
109
|
+
data = MockResponse("ffff")
|
|
110
|
+
self.assertEqual(0, testee.read(data))
|
|
111
|
+
|
|
112
|
+
def test_current_signed(self):
|
|
113
|
+
testee = CurrentS("", 0, "", None)
|
|
114
|
+
|
|
115
|
+
data = MockResponse("0031")
|
|
116
|
+
self.assertEqual(4.9, testee.read(data))
|
|
117
|
+
self.assertEqual("0031", testee.encode_value(4.9).hex())
|
|
118
|
+
|
|
88
119
|
data = MockResponse("ff9e")
|
|
89
120
|
self.assertEqual(-9.8, testee.read(data))
|
|
90
121
|
self.assertEqual("ff9e", testee.encode_value(-9.8).hex())
|
|
@@ -95,6 +126,18 @@ class TestUtils(TestCase):
|
|
|
95
126
|
data = MockResponse("0000069f")
|
|
96
127
|
self.assertEqual(1695, testee.read(data))
|
|
97
128
|
|
|
129
|
+
data = MockResponse("fffffffd")
|
|
130
|
+
self.assertEqual(4294967293, testee.read(data))
|
|
131
|
+
|
|
132
|
+
data = MockResponse("ffffffff")
|
|
133
|
+
self.assertEqual(0, testee.read(data))
|
|
134
|
+
|
|
135
|
+
def test_power4_signed(self):
|
|
136
|
+
testee = Power4S("", 0, "", None)
|
|
137
|
+
|
|
138
|
+
data = MockResponse("0000069f")
|
|
139
|
+
self.assertEqual(1695, testee.read(data))
|
|
140
|
+
|
|
98
141
|
data = MockResponse("fffffffd")
|
|
99
142
|
self.assertEqual(-3, testee.read(data))
|
|
100
143
|
|
|
@@ -110,7 +153,7 @@ class TestUtils(TestCase):
|
|
|
110
153
|
data = MockResponse("00020972")
|
|
111
154
|
self.assertEqual(13349.0, testee.read(data))
|
|
112
155
|
data = MockResponse("ffffffff")
|
|
113
|
-
self.
|
|
156
|
+
self.assertEqual(0.0, testee.read(data))
|
|
114
157
|
|
|
115
158
|
def test_timestamp(self):
|
|
116
159
|
testee = Timestamp("", 0, "", None)
|
|
@@ -126,7 +169,6 @@ class TestUtils(TestCase):
|
|
|
126
169
|
data = MockResponse("0d1e0e28ffc4ff1a")
|
|
127
170
|
self.assertEqual("13:30-14:40 Mon,Wed,Thu -60% On", testee.read(data).__str__())
|
|
128
171
|
self.assertEqual(bytes.fromhex("0d1e0e28ffc4ff1a"), testee.encode_value(bytes.fromhex("0d1e0e28ffc4ff1a")))
|
|
129
|
-
self.assertRaises(ValueError, lambda: testee.encode_value(bytes.fromhex("0d1e0e28ffc4ffff")))
|
|
130
172
|
self.assertRaises(ValueError, lambda: testee.encode_value("some string"))
|
|
131
173
|
self.assertFalse(testee.read(data).is_eco_charge_mode())
|
|
132
174
|
self.assertFalse(testee.read(data).is_eco_discharge_mode())
|
|
@@ -156,7 +198,6 @@ class TestUtils(TestCase):
|
|
|
156
198
|
self.assertEqual(ScheduleType.ECO_MODE, testee.schedule_type)
|
|
157
199
|
self.assertEqual(bytes.fromhex("0d1e0e28ff1affc4005a0000"),
|
|
158
200
|
testee.encode_value(bytes.fromhex("0d1e0e28ff1affc4005a0000")))
|
|
159
|
-
self.assertRaises(ValueError, lambda: testee.encode_value(bytes.fromhex("0d1e0e28ffffffc4005a0000")))
|
|
160
201
|
self.assertRaises(ValueError, lambda: testee.encode_value("some string"))
|
|
161
202
|
self.assertFalse(testee.read(data).is_eco_charge_mode())
|
|
162
203
|
self.assertFalse(testee.read(data).is_eco_discharge_mode())
|
|
@@ -176,6 +217,8 @@ class TestUtils(TestCase):
|
|
|
176
217
|
self.assertFalse(testee.read(data).is_eco_charge_mode())
|
|
177
218
|
self.assertFalse(testee.read(data).is_eco_discharge_mode())
|
|
178
219
|
|
|
220
|
+
data = MockResponse("0300080006fefd12005fcfff")
|
|
221
|
+
self.assertEqual("3:0-8:0 Mon -75% (SoC 95%) Off", testee.read(data).__str__())
|
|
179
222
|
data = MockResponse("0000173b5500001400640000")
|
|
180
223
|
self.assertEqual("0:0-23:59 20% (SoC 100%) Unset", testee.read(data).__str__())
|
|
181
224
|
data = MockResponse("ffffffff557f000000010001")
|
goodwe-0.3.1/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.1
|
goodwe-0.3.1/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
|