goodwe 0.3.2__tar.gz → 0.3.4__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.4}/PKG-INFO +1 -1
- goodwe-0.3.4/VERSION +1 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/es.py +13 -7
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/et.py +41 -16
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/inverter.py +3 -2
- goodwe-0.3.4/goodwe/model.py +50 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/sensor.py +82 -22
- {goodwe-0.3.2 → goodwe-0.3.4/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.3.2 → goodwe-0.3.4}/tests/test_dt.py +7 -7
- {goodwe-0.3.2 → goodwe-0.3.4}/tests/test_et.py +19 -19
- {goodwe-0.3.2 → goodwe-0.3.4}/tests/test_sensor.py +15 -4
- goodwe-0.3.2/VERSION +0 -1
- goodwe-0.3.2/goodwe/model.py +0 -40
- {goodwe-0.3.2 → goodwe-0.3.4}/LICENSE +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/README.md +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/__init__.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/const.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/dt.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/exceptions.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/modbus.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe/protocol.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/pyproject.toml +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/setup.cfg +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/tests/test_es.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/tests/test_modbus.py +0 -0
- {goodwe-0.3.2 → goodwe-0.3.4}/tests/test_protocol.py +0 -0
goodwe-0.3.4/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.4
|
|
@@ -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")
|
|
@@ -95,7 +95,7 @@ class ES(Inverter):
|
|
|
95
95
|
Power("pback_up", 81, "Back-up Power", Kind.UPS),
|
|
96
96
|
# pload + pback_up
|
|
97
97
|
Calculated("plant_power",
|
|
98
|
-
lambda data: round(read_bytes2(data, 47) + read_bytes2(data, 81)),
|
|
98
|
+
lambda data: round(read_bytes2(data, 47, 0) + read_bytes2(data, 81, 0)),
|
|
99
99
|
"Plant Power", "W", Kind.AC),
|
|
100
100
|
Decimal("meter_power_factor", 83, 1000, "Meter Power Factor", "", Kind.GRID), # modbus 0x531
|
|
101
101
|
# Integer("xx85", 85, "Unknown sensor@85"),
|
|
@@ -135,7 +135,7 @@ class ES(Inverter):
|
|
|
135
135
|
Integer("charge_i", 26, "Charge Current", "A", ),
|
|
136
136
|
Integer("discharge_i", 28, "Discharge Current", "A", ),
|
|
137
137
|
Decimal("discharge_v", 30, 10, "Discharge Voltage", "V"),
|
|
138
|
-
Calculated("dod", lambda data: 100 - read_bytes2(data, 32), "Depth of Discharge", "%"),
|
|
138
|
+
Calculated("dod", lambda data: 100 - read_bytes2(data, 32, 0), "Depth of Discharge", "%"),
|
|
139
139
|
Integer("battery_activated", 34, "Battery Activated"),
|
|
140
140
|
Integer("bp_off_grid_charge", 36, "BP Off-grid Charge"),
|
|
141
141
|
Integer("bp_pv_discharge", 38, "BP PV Discharge"),
|
|
@@ -285,19 +285,25 @@ class ES(Inverter):
|
|
|
285
285
|
async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
|
|
286
286
|
result = [e for e in OperationMode]
|
|
287
287
|
result.remove(OperationMode.PEAK_SHAVING)
|
|
288
|
+
result.remove(OperationMode.SELF_USE)
|
|
288
289
|
if not include_emulated:
|
|
289
290
|
result.remove(OperationMode.ECO_CHARGE)
|
|
290
291
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
291
292
|
return tuple(result)
|
|
292
293
|
|
|
293
294
|
async def get_operation_mode(self) -> OperationMode:
|
|
294
|
-
|
|
295
|
+
mode_id = await self.read_setting('work_mode')
|
|
296
|
+
try:
|
|
297
|
+
mode = OperationMode(mode_id)
|
|
298
|
+
except ValueError:
|
|
299
|
+
logger.debug("Unknown work_mode value %d", mode_id)
|
|
300
|
+
return None
|
|
295
301
|
if OperationMode.ECO != mode:
|
|
296
302
|
return mode
|
|
297
|
-
|
|
298
|
-
if
|
|
303
|
+
eco_mode = await self.read_setting('eco_mode_1')
|
|
304
|
+
if eco_mode.is_eco_charge_mode():
|
|
299
305
|
return OperationMode.ECO_CHARGE
|
|
300
|
-
elif
|
|
306
|
+
elif eco_mode.is_eco_discharge_mode():
|
|
301
307
|
return OperationMode.ECO_DISCHARGE
|
|
302
308
|
else:
|
|
303
309
|
return OperationMode.ECO
|
|
@@ -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, ...] = (
|
|
@@ -35,10 +35,10 @@ class ET(Inverter):
|
|
|
35
35
|
# ppv1 + ppv2 + ppv3 + ppv4
|
|
36
36
|
Calculated("ppv",
|
|
37
37
|
lambda data:
|
|
38
|
-
max(0, read_bytes4(data, 35105)) +
|
|
39
|
-
max(0, read_bytes4(data, 35109)) +
|
|
40
|
-
max(0, read_bytes4(data, 35113)) +
|
|
41
|
-
max(0, read_bytes4(data, 35117)),
|
|
38
|
+
max(0, read_bytes4(data, 35105, 0)) +
|
|
39
|
+
max(0, read_bytes4(data, 35109, 0)) +
|
|
40
|
+
max(0, read_bytes4(data, 35113, 0)) +
|
|
41
|
+
max(0, read_bytes4(data, 35117, 0)),
|
|
42
42
|
"PV Power", "W", Kind.PV),
|
|
43
43
|
ByteH("pv4_mode", 35119, "PV4 Mode code", "", Kind.PV),
|
|
44
44
|
EnumH("pv4_mode_label", 35119, PV_MODES, "PV4 Mode", Kind.PV),
|
|
@@ -145,10 +145,10 @@ class ET(Inverter):
|
|
|
145
145
|
# ppv1 + ppv2 + ppv3 + ppv4 + pbattery1 - active_power
|
|
146
146
|
Calculated("house_consumption",
|
|
147
147
|
lambda data:
|
|
148
|
-
read_bytes4(data, 35105) +
|
|
149
|
-
read_bytes4(data, 35109) +
|
|
150
|
-
read_bytes4(data, 35113) +
|
|
151
|
-
read_bytes4(data, 35117) +
|
|
148
|
+
read_bytes4(data, 35105, 0) +
|
|
149
|
+
read_bytes4(data, 35109, 0) +
|
|
150
|
+
read_bytes4(data, 35113, 0) +
|
|
151
|
+
read_bytes4(data, 35117, 0) +
|
|
152
152
|
read_bytes4_signed(data, 35182) -
|
|
153
153
|
read_bytes2_signed(data, 35140),
|
|
154
154
|
"House Consumption", "W", Kind.AC),
|
|
@@ -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):
|
|
@@ -598,19 +604,26 @@ class ET(Inverter):
|
|
|
598
604
|
result = [e for e in OperationMode]
|
|
599
605
|
if not self._has_peak_shaving:
|
|
600
606
|
result.remove(OperationMode.PEAK_SHAVING)
|
|
607
|
+
if not is_745_platform(self):
|
|
608
|
+
result.remove(OperationMode.SELF_USE)
|
|
601
609
|
if not include_emulated:
|
|
602
610
|
result.remove(OperationMode.ECO_CHARGE)
|
|
603
611
|
result.remove(OperationMode.ECO_DISCHARGE)
|
|
604
612
|
return tuple(result)
|
|
605
613
|
|
|
606
614
|
async def get_operation_mode(self) -> OperationMode:
|
|
607
|
-
|
|
615
|
+
mode_id = await self.read_setting('work_mode')
|
|
616
|
+
try:
|
|
617
|
+
mode = OperationMode(mode_id)
|
|
618
|
+
except ValueError:
|
|
619
|
+
logger.debug("Unknown work_mode value %d", mode_id)
|
|
620
|
+
return None
|
|
608
621
|
if OperationMode.ECO != mode:
|
|
609
622
|
return mode
|
|
610
|
-
|
|
611
|
-
if
|
|
623
|
+
eco_mode = await self.read_setting('eco_mode_1')
|
|
624
|
+
if eco_mode.is_eco_charge_mode():
|
|
612
625
|
return OperationMode.ECO_CHARGE
|
|
613
|
-
elif
|
|
626
|
+
elif eco_mode.is_eco_discharge_mode():
|
|
614
627
|
return OperationMode.ECO_DISCHARGE
|
|
615
628
|
else:
|
|
616
629
|
return OperationMode.ECO
|
|
@@ -626,6 +639,7 @@ class ET(Inverter):
|
|
|
626
639
|
await self._set_offline(True)
|
|
627
640
|
await self.write_setting('backup_supply', 1)
|
|
628
641
|
await self.write_setting('cold_start', 4)
|
|
642
|
+
await self._clear_battery_mode_param()
|
|
629
643
|
elif operation_mode == OperationMode.BACKUP:
|
|
630
644
|
await self.write_setting('work_mode', 2)
|
|
631
645
|
await self._set_offline(False)
|
|
@@ -636,13 +650,24 @@ class ET(Inverter):
|
|
|
636
650
|
elif operation_mode == OperationMode.PEAK_SHAVING:
|
|
637
651
|
await self.write_setting('work_mode', 4)
|
|
638
652
|
await self._set_offline(False)
|
|
653
|
+
await self._clear_battery_mode_param()
|
|
654
|
+
elif operation_mode == OperationMode.SELF_USE:
|
|
655
|
+
await self.write_setting('work_mode', 5)
|
|
656
|
+
await self._set_offline(False)
|
|
657
|
+
await self._clear_battery_mode_param()
|
|
639
658
|
elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE):
|
|
640
659
|
if eco_mode_power < 0 or eco_mode_power > 100:
|
|
641
660
|
raise ValueError()
|
|
642
661
|
if eco_mode_soc < 0 or eco_mode_soc > 100:
|
|
643
662
|
raise ValueError()
|
|
663
|
+
|
|
644
664
|
eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
|
|
645
|
-
|
|
665
|
+
# Load the current values to try to detect schedule type
|
|
666
|
+
try:
|
|
667
|
+
await self._read_setting(eco_mode)
|
|
668
|
+
except ValueError:
|
|
669
|
+
pass
|
|
670
|
+
eco_mode.set_schedule_type(ScheduleType.ECO_MODE, is_745_platform(self))
|
|
646
671
|
if operation_mode == OperationMode.ECO_CHARGE:
|
|
647
672
|
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
|
|
648
673
|
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
|
|
@@ -179,7 +183,7 @@ class Energy(Sensor):
|
|
|
179
183
|
|
|
180
184
|
def read_value(self, data: ProtocolResponse):
|
|
181
185
|
value = read_bytes2(data)
|
|
182
|
-
return float(value) / 10
|
|
186
|
+
return float(value) / 10 if value else None
|
|
183
187
|
|
|
184
188
|
|
|
185
189
|
class Energy4(Sensor):
|
|
@@ -190,7 +194,7 @@ class Energy4(Sensor):
|
|
|
190
194
|
|
|
191
195
|
def read_value(self, data: ProtocolResponse):
|
|
192
196
|
value = read_bytes4(data)
|
|
193
|
-
return float(value) / 10
|
|
197
|
+
return float(value) / 10 if value else None
|
|
194
198
|
|
|
195
199
|
|
|
196
200
|
class Apparent(Sensor):
|
|
@@ -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, None, 0)
|
|
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, None, 0)
|
|
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):
|
|
@@ -384,7 +414,7 @@ class EnumH(Sensor):
|
|
|
384
414
|
|
|
385
415
|
|
|
386
416
|
class EnumL(Sensor):
|
|
387
|
-
"""Sensor representing label from enumeration encoded in 1
|
|
417
|
+
"""Sensor representing label from enumeration encoded in 1 byte (low 8 bits of 16bit register)"""
|
|
388
418
|
|
|
389
419
|
def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None):
|
|
390
420
|
super().__init__(id_, offset, name, 1, "", kind)
|
|
@@ -403,7 +433,7 @@ class Enum2(Sensor):
|
|
|
403
433
|
self._labels: Dict = labels
|
|
404
434
|
|
|
405
435
|
def read_value(self, data: ProtocolResponse):
|
|
406
|
-
return self._labels.get(read_bytes2(data))
|
|
436
|
+
return self._labels.get(read_bytes2(data, None, 0))
|
|
407
437
|
|
|
408
438
|
|
|
409
439
|
class EnumBitmap4(Sensor):
|
|
@@ -434,7 +464,8 @@ class EnumBitmap22(Sensor):
|
|
|
434
464
|
raise NotImplementedError()
|
|
435
465
|
|
|
436
466
|
def read(self, data: ProtocolResponse):
|
|
437
|
-
return decode_bitmap(read_bytes2(data, self.offset) << 16 + read_bytes2(data, self._offsetL
|
|
467
|
+
return decode_bitmap(read_bytes2(data, self.offset, 0) << 16 + read_bytes2(data, self._offsetL, 0),
|
|
468
|
+
self._labels)
|
|
438
469
|
|
|
439
470
|
|
|
440
471
|
class EnumCalculated(Sensor):
|
|
@@ -476,6 +507,14 @@ class EcoMode(ABC):
|
|
|
476
507
|
def is_eco_discharge_mode(self) -> bool:
|
|
477
508
|
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
|
|
478
509
|
|
|
510
|
+
@abstractmethod
|
|
511
|
+
def get_schedule_type(self) -> ScheduleType:
|
|
512
|
+
"""Answer the schedule type"""
|
|
513
|
+
|
|
514
|
+
@abstractmethod
|
|
515
|
+
def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
|
|
516
|
+
"""Set the schedule type"""
|
|
517
|
+
|
|
479
518
|
|
|
480
519
|
class EcoModeV1(Sensor, EcoMode):
|
|
481
520
|
"""Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
|
|
@@ -518,8 +557,6 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
518
557
|
raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
|
|
519
558
|
self.day_bits = read_byte(data)
|
|
520
559
|
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
560
|
return self
|
|
524
561
|
|
|
525
562
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
@@ -561,6 +598,14 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
561
598
|
and self.day_bits == 127 \
|
|
562
599
|
and self.power > 0
|
|
563
600
|
|
|
601
|
+
def get_schedule_type(self) -> ScheduleType:
|
|
602
|
+
"""Answer the schedule type"""
|
|
603
|
+
return ScheduleType.ECO_MODE
|
|
604
|
+
|
|
605
|
+
def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
|
|
606
|
+
"""Set the schedule type"""
|
|
607
|
+
pass
|
|
608
|
+
|
|
564
609
|
def as_eco_mode_v2(self) -> EcoModeV2:
|
|
565
610
|
"""Convert V1 to V2 EcoMode"""
|
|
566
611
|
result = EcoModeV2(self.id_, self.offset, self.name)
|
|
@@ -617,8 +662,6 @@ class Schedule(Sensor, EcoMode):
|
|
|
617
662
|
self.schedule_type = ScheduleType.detect_schedule_type(self.on_off)
|
|
618
663
|
self.day_bits = read_byte(data)
|
|
619
664
|
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
665
|
self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
|
|
623
666
|
if not self.schedule_type.is_in_range(self.power):
|
|
624
667
|
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
|
|
@@ -680,6 +723,19 @@ class Schedule(Sensor, EcoMode):
|
|
|
680
723
|
and self.power > 0 \
|
|
681
724
|
and (self.month_bits == 0 or self.month_bits == 0x0fff)
|
|
682
725
|
|
|
726
|
+
def get_schedule_type(self) -> ScheduleType:
|
|
727
|
+
"""Answer the schedule type"""
|
|
728
|
+
return self.schedule_type
|
|
729
|
+
|
|
730
|
+
def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
|
|
731
|
+
"""Set the schedule type"""
|
|
732
|
+
if schedule_type == ScheduleType.ECO_MODE:
|
|
733
|
+
# try to keep-reuse the type, use is745 only when necessary
|
|
734
|
+
if self.schedule_type not in (ScheduleType.ECO_MODE, ScheduleType.ECO_MODE_745):
|
|
735
|
+
self.schedule_type = ScheduleType.ECO_MODE_745 if is745 else ScheduleType.ECO_MODE
|
|
736
|
+
else:
|
|
737
|
+
self.schedule_type = schedule_type
|
|
738
|
+
|
|
683
739
|
def as_eco_mode_v1(self) -> EcoModeV1:
|
|
684
740
|
"""Convert V2 to V1 EcoMode"""
|
|
685
741
|
result = EcoModeV1(self.id_, self.offset, self.name)
|
|
@@ -730,12 +786,12 @@ def read_byte(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
730
786
|
return int.from_bytes(buffer.read(1), byteorder="big", signed=True)
|
|
731
787
|
|
|
732
788
|
|
|
733
|
-
def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
789
|
+
def read_bytes2(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
|
|
734
790
|
"""Retrieve 2 byte (unsigned int) value from buffer"""
|
|
735
791
|
if offset is not None:
|
|
736
792
|
buffer.seek(offset)
|
|
737
793
|
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
738
|
-
return
|
|
794
|
+
return undef if value == 0xffff else value
|
|
739
795
|
|
|
740
796
|
|
|
741
797
|
def read_bytes2_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
@@ -745,12 +801,12 @@ def read_bytes2_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
745
801
|
return int.from_bytes(buffer.read(2), byteorder="big", signed=True)
|
|
746
802
|
|
|
747
803
|
|
|
748
|
-
def read_bytes4(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
804
|
+
def read_bytes4(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
|
|
749
805
|
"""Retrieve 4 byte (unsigned int) value from buffer"""
|
|
750
806
|
if offset is not None:
|
|
751
807
|
buffer.seek(offset)
|
|
752
808
|
value = int.from_bytes(buffer.read(4), byteorder="big", signed=False)
|
|
753
|
-
return
|
|
809
|
+
return undef if value == 0xffffffff else value
|
|
754
810
|
|
|
755
811
|
|
|
756
812
|
def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
@@ -891,6 +947,10 @@ def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str:
|
|
|
891
947
|
|
|
892
948
|
|
|
893
949
|
def decode_day_of_week(data: int) -> str:
|
|
950
|
+
if data == -1:
|
|
951
|
+
return "Mon-Sun"
|
|
952
|
+
elif data == 0:
|
|
953
|
+
return ""
|
|
894
954
|
bits = bin(data)[2:]
|
|
895
955
|
daynames = list(DAY_NAMES)
|
|
896
956
|
days = ""
|
|
@@ -904,7 +964,7 @@ def decode_day_of_week(data: int) -> str:
|
|
|
904
964
|
|
|
905
965
|
|
|
906
966
|
def decode_months(data: int) -> str | None:
|
|
907
|
-
if data
|
|
967
|
+
if data <= 0 or data == 0x0fff:
|
|
908
968
|
return None
|
|
909
969
|
bits = bin(data)[2:]
|
|
910
970
|
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}")
|
|
@@ -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', None, 'kWh', data)
|
|
171
|
+
self.assertSensor('e_total', None, '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)
|
|
@@ -221,7 +221,7 @@ class GW5000D_NS_Test(DtMock):
|
|
|
221
221
|
self.assertSensor("apparent_power", -1, "VA", data),
|
|
222
222
|
self.assertSensor("reactive_power", -1, "var", data),
|
|
223
223
|
self.assertSensor('temperature', 1.4, 'C', data)
|
|
224
|
-
self.assertSensor('e_day',
|
|
224
|
+
self.assertSensor('e_day', None, 'kWh', data)
|
|
225
225
|
self.assertSensor('e_total', 881.7, 'kWh', data)
|
|
226
226
|
self.assertSensor('h_total', 955, 'h', data)
|
|
227
227
|
self.assertSensor('safety_country', 73, '', 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
|
|
|
@@ -164,7 +164,7 @@ class GW10K_ET_Test(EtMock):
|
|
|
164
164
|
self.assertSensor('h_total', 9246, 'h', data)
|
|
165
165
|
self.assertSensor("e_day_exp", 9.8, 'kWh', data)
|
|
166
166
|
self.assertSensor("e_total_imp", 58.0, 'kWh', data)
|
|
167
|
-
self.assertSensor("e_day_imp",
|
|
167
|
+
self.assertSensor("e_day_imp", None, 'kWh', data)
|
|
168
168
|
self.assertSensor("e_load_total", 8820.2, 'kWh', data)
|
|
169
169
|
self.assertSensor("e_load_day", 11.6, 'kWh', data)
|
|
170
170
|
self.assertSensor("e_bat_charge_total", 2758.1, 'kWh', data)
|
|
@@ -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)
|
|
@@ -457,14 +457,14 @@ class GW6000_EH_Test(EtMock):
|
|
|
457
457
|
self.assertSensor("e_total_exp", 58.6, 'kWh', data)
|
|
458
458
|
self.assertSensor('h_total', 33, 'h', data)
|
|
459
459
|
self.assertSensor("e_day_exp", 21.6, 'kWh', data)
|
|
460
|
-
self.assertSensor("e_total_imp",
|
|
461
|
-
self.assertSensor("e_day_imp",
|
|
460
|
+
self.assertSensor("e_total_imp", None, 'kWh', data)
|
|
461
|
+
self.assertSensor("e_day_imp", None, 'kWh', data)
|
|
462
462
|
self.assertSensor("e_load_total", 70.1, 'kWh', data)
|
|
463
463
|
self.assertSensor("e_load_day", 27.1, 'kWh', data)
|
|
464
|
-
self.assertSensor("e_bat_charge_total",
|
|
465
|
-
self.assertSensor("e_bat_charge_day",
|
|
466
|
-
self.assertSensor("e_bat_discharge_total",
|
|
467
|
-
self.assertSensor("e_bat_discharge_day",
|
|
464
|
+
self.assertSensor("e_bat_charge_total", None, 'kWh', data)
|
|
465
|
+
self.assertSensor("e_bat_charge_day", None, 'kWh', data)
|
|
466
|
+
self.assertSensor("e_bat_discharge_total", None, 'kWh', data)
|
|
467
|
+
self.assertSensor("e_bat_discharge_day", None, 'kWh', data)
|
|
468
468
|
self.assertSensor('diagnose_result', 117983303, '', data)
|
|
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',
|
|
@@ -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)
|
|
@@ -558,8 +558,8 @@ class GEH10_1U_10_Test(EtMock):
|
|
|
558
558
|
self.assertSensor('e_total_exp', 10273.3, 'kWh', data)
|
|
559
559
|
self.assertSensor('h_total', 3256, 'h', data)
|
|
560
560
|
self.assertSensor('e_day_exp', 16.6, 'kWh', data)
|
|
561
|
-
self.assertSensor('e_total_imp',
|
|
562
|
-
self.assertSensor('e_day_imp',
|
|
561
|
+
self.assertSensor('e_total_imp', None, 'kWh', data)
|
|
562
|
+
self.assertSensor('e_day_imp', None, 'kWh', data)
|
|
563
563
|
self.assertSensor('e_load_total', 4393.9, 'kWh', data)
|
|
564
564
|
self.assertSensor('e_load_day', 10.7, 'kWh', data)
|
|
565
565
|
self.assertSensor('e_bat_charge_total', 141.9, 'kWh', data)
|
|
@@ -771,7 +771,7 @@ class GW25K_ET_Test(EtMock):
|
|
|
771
771
|
self.assertSensor('e_bat_charge_total', 91.3, 'kWh', data)
|
|
772
772
|
self.assertSensor('e_bat_charge_day', 11.0, 'kWh', data)
|
|
773
773
|
self.assertSensor('e_bat_discharge_total', 69.6, 'kWh', data)
|
|
774
|
-
self.assertSensor('e_bat_discharge_day',
|
|
774
|
+
self.assertSensor('e_bat_discharge_day', None, 'kWh', data)
|
|
775
775
|
self.assertSensor('diagnose_result', 33816960, '', data)
|
|
776
776
|
self.assertSensor('diagnose_result_label',
|
|
777
777
|
'BMS: Discharge current low, APP: Discharge current too low, BMS: Charge disabled, PF value set',
|
|
@@ -1042,13 +1042,13 @@ class GW29K9_ET_Test(EtMock):
|
|
|
1042
1042
|
self.assertSensor('h_total', 1175, 'h', data)
|
|
1043
1043
|
self.assertSensor('e_day_exp', 1.2, 'kWh', data)
|
|
1044
1044
|
self.assertSensor('e_total_imp', 8.7, 'kWh', data)
|
|
1045
|
-
self.assertSensor('e_day_imp',
|
|
1045
|
+
self.assertSensor('e_day_imp', None, 'kWh', data)
|
|
1046
1046
|
self.assertSensor('e_load_total', 10742.2, 'kWh', data)
|
|
1047
1047
|
self.assertSensor('e_load_day', 43.8, 'kWh', data)
|
|
1048
|
-
self.assertSensor('e_bat_charge_total',
|
|
1049
|
-
self.assertSensor('e_bat_charge_day',
|
|
1050
|
-
self.assertSensor('e_bat_discharge_total',
|
|
1051
|
-
self.assertSensor('e_bat_discharge_day',
|
|
1048
|
+
self.assertSensor('e_bat_charge_total', None, 'kWh', data)
|
|
1049
|
+
self.assertSensor('e_bat_charge_day', None, 'kWh', data)
|
|
1050
|
+
self.assertSensor('e_bat_discharge_total', None, 'kWh', data)
|
|
1051
|
+
self.assertSensor('e_bat_discharge_day', None, 'kWh', data)
|
|
1052
1052
|
self.assertSensor('diagnose_result', 33816782, '', data)
|
|
1053
1053
|
self.assertSensor('diagnose_result_label',
|
|
1054
1054
|
'Battery SOC low, Battery SOC in back, BMS: Discharge disabled, '
|
|
@@ -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())
|
|
@@ -119,7 +130,7 @@ class TestUtils(TestCase):
|
|
|
119
130
|
self.assertEqual(4294967293, testee.read(data))
|
|
120
131
|
|
|
121
132
|
data = MockResponse("ffffffff")
|
|
122
|
-
self.
|
|
133
|
+
self.assertIsNone(testee.read(data))
|
|
123
134
|
|
|
124
135
|
def test_power4_signed(self):
|
|
125
136
|
testee = Power4S("", 0, "", None)
|
|
@@ -142,7 +153,7 @@ class TestUtils(TestCase):
|
|
|
142
153
|
data = MockResponse("00020972")
|
|
143
154
|
self.assertEqual(13349.0, testee.read(data))
|
|
144
155
|
data = MockResponse("ffffffff")
|
|
145
|
-
self.
|
|
156
|
+
self.assertIsNone(testee.read(data))
|
|
146
157
|
|
|
147
158
|
def test_timestamp(self):
|
|
148
159
|
testee = Timestamp("", 0, "", None)
|
|
@@ -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
|