goodwe 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl
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/es.py +40 -41
- goodwe/et.py +54 -46
- goodwe/protocol.py +20 -1
- goodwe/sensor.py +214 -105
- {goodwe-0.3.0.dist-info → goodwe-0.3.2.dist-info}/METADATA +1 -1
- goodwe-0.3.2.dist-info/RECORD +16 -0
- {goodwe-0.3.0.dist-info → goodwe-0.3.2.dist-info}/WHEEL +1 -1
- goodwe-0.3.0.dist-info/RECORD +0 -16
- {goodwe-0.3.0.dist-info → goodwe-0.3.2.dist-info}/LICENSE +0 -0
- {goodwe-0.3.0.dist-info → goodwe-0.3.2.dist-info}/top_level.txt +0 -0
goodwe/es.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Tuple
|
|
4
|
+
from typing import Tuple
|
|
5
5
|
|
|
6
6
|
from .exceptions import InverterError
|
|
7
7
|
from .inverter import Inverter
|
|
@@ -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
|
|
|
@@ -220,17 +220,20 @@ class ES(Inverter):
|
|
|
220
220
|
setting: Sensor | None = self._settings.get(setting_id)
|
|
221
221
|
if not setting:
|
|
222
222
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
223
|
-
|
|
224
|
-
if self._is_modbus_setting(setting):
|
|
225
|
-
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
|
|
226
|
-
return setting.read_value(response)
|
|
227
|
-
else:
|
|
228
|
-
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count))
|
|
229
|
-
return setting.read_value(response)
|
|
223
|
+
return await self._read_setting(setting)
|
|
230
224
|
else:
|
|
231
225
|
all_settings = await self.read_settings_data()
|
|
232
226
|
return all_settings.get(setting_id)
|
|
233
227
|
|
|
228
|
+
async def _read_setting(self, setting: Sensor) -> Any:
|
|
229
|
+
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
230
|
+
if self._is_modbus_setting(setting):
|
|
231
|
+
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
|
|
232
|
+
return setting.read_value(response)
|
|
233
|
+
else:
|
|
234
|
+
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count))
|
|
235
|
+
return setting.read_value(response)
|
|
236
|
+
|
|
234
237
|
async def write_setting(self, setting_id: str, value: Any):
|
|
235
238
|
if setting_id == 'time':
|
|
236
239
|
await self._read_from_socket(
|
|
@@ -240,27 +243,30 @@ class ES(Inverter):
|
|
|
240
243
|
setting: Sensor | None = self._settings.get(setting_id)
|
|
241
244
|
if not setting:
|
|
242
245
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
await self._write_setting(setting, value)
|
|
247
|
+
|
|
248
|
+
async def _write_setting(self, setting: Sensor, value: Any):
|
|
249
|
+
if setting.size_ == 1:
|
|
250
|
+
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
251
|
+
if self._is_modbus_setting(setting):
|
|
252
|
+
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
|
|
253
|
+
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
251
254
|
else:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
255
|
+
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
|
|
256
|
+
raw_value = setting.encode_value(value, response.response_data()[2:4])
|
|
257
|
+
else:
|
|
258
|
+
raw_value = setting.encode_value(value)
|
|
259
|
+
if len(raw_value) <= 2:
|
|
260
|
+
value = int.from_bytes(raw_value, byteorder="big", signed=True)
|
|
261
|
+
if self._is_modbus_setting(setting):
|
|
262
|
+
await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
|
|
263
|
+
else:
|
|
264
|
+
await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
|
|
265
|
+
else:
|
|
266
|
+
if self._is_modbus_setting(setting):
|
|
267
|
+
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
|
|
259
268
|
else:
|
|
260
|
-
|
|
261
|
-
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
|
|
262
|
-
else:
|
|
263
|
-
await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))
|
|
269
|
+
await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))
|
|
264
270
|
|
|
265
271
|
async def read_settings_data(self) -> Dict[str, Any]:
|
|
266
272
|
response = await self._read_from_socket(self._READ_DEVICE_SETTINGS_DATA)
|
|
@@ -313,7 +319,8 @@ class ES(Inverter):
|
|
|
313
319
|
raise ValueError()
|
|
314
320
|
if eco_mode_soc < 0 or eco_mode_soc > 100:
|
|
315
321
|
raise ValueError()
|
|
316
|
-
eco_mode: EcoMode = self.
|
|
322
|
+
eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
|
|
323
|
+
await self._read_setting(eco_mode)
|
|
317
324
|
if operation_mode == OperationMode.ECO_CHARGE:
|
|
318
325
|
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
|
|
319
326
|
else:
|
|
@@ -327,7 +334,7 @@ class ES(Inverter):
|
|
|
327
334
|
return await self.read_setting('dod')
|
|
328
335
|
|
|
329
336
|
async def set_ongrid_battery_dod(self, dod: int) -> None:
|
|
330
|
-
if 0 <= dod <=
|
|
337
|
+
if 0 <= dod <= 100:
|
|
331
338
|
await self._read_from_socket(Aa55WriteCommand(0x560, 100 - dod))
|
|
332
339
|
|
|
333
340
|
async def _reset_inverter(self) -> None:
|
|
@@ -427,13 +434,5 @@ class ES(Inverter):
|
|
|
427
434
|
async def _set_work_mode(self, mode: int) -> None:
|
|
428
435
|
await self._read_from_socket(Aa55ProtocolCommand("035901" + "{:02x}".format(mode), "03D9"))
|
|
429
436
|
|
|
430
|
-
def _convert_eco_mode(self, sensor: Sensor) -> Sensor | EcoMode:
|
|
431
|
-
if EcoModeV1 == type(sensor) and self._supports_eco_mode_v2():
|
|
432
|
-
return cast(EcoModeV1, sensor).as_eco_mode_v2()
|
|
433
|
-
elif EcoModeV2 == type(sensor) and not self._supports_eco_mode_v2():
|
|
434
|
-
return cast(EcoModeV2, sensor).as_eco_mode_v1()
|
|
435
|
-
else:
|
|
436
|
-
return sensor
|
|
437
|
-
|
|
438
437
|
def _is_modbus_setting(self, sensor: Sensor) -> bool:
|
|
439
|
-
return
|
|
438
|
+
return sensor.offset > 30000
|
goodwe/et.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Tuple
|
|
4
|
+
from typing import Tuple
|
|
5
5
|
|
|
6
6
|
from .exceptions import RequestRejectedException
|
|
7
7
|
from .inverter import Inverter
|
|
@@ -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"),
|
|
@@ -411,6 +411,8 @@ class ET(Inverter):
|
|
|
411
411
|
self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018)
|
|
412
412
|
self._READ_BATTERY2_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9858, 0x0016)
|
|
413
413
|
self._READ_MPPT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89e5, 0x3d)
|
|
414
|
+
self._has_eco_mode_v2: bool = True
|
|
415
|
+
self._has_peak_shaving: bool = True
|
|
414
416
|
self._has_battery: bool = True
|
|
415
417
|
self._has_battery2: bool = False
|
|
416
418
|
self._has_meter_extended: bool = False
|
|
@@ -422,12 +424,6 @@ class ET(Inverter):
|
|
|
422
424
|
self._sensors_mppt = self.__all_sensors_mppt
|
|
423
425
|
self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
|
|
424
426
|
|
|
425
|
-
def _supports_eco_mode_v2(self) -> bool:
|
|
426
|
-
return self.arm_version >= 19 or 'ETU' not in self.serial_number
|
|
427
|
-
|
|
428
|
-
def _supports_peak_shaving(self) -> bool:
|
|
429
|
-
return self.arm_version >= 22 or 'ETU' not in self.serial_number
|
|
430
|
-
|
|
431
427
|
@staticmethod
|
|
432
428
|
def _single_phase_only(s: Sensor) -> bool:
|
|
433
429
|
"""Filter to exclude phase2/3 sensors on single phase inverters"""
|
|
@@ -474,10 +470,23 @@ class ET(Inverter):
|
|
|
474
470
|
else:
|
|
475
471
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
476
472
|
|
|
477
|
-
|
|
473
|
+
# Check and add EcoModeV2 settings added in (ETU fw 19)
|
|
474
|
+
try:
|
|
475
|
+
await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47547, 6))
|
|
478
476
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
|
|
479
|
-
|
|
477
|
+
except RequestRejectedException as ex:
|
|
478
|
+
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
479
|
+
logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
|
|
480
|
+
self._has_eco_mode_v2 = False
|
|
481
|
+
|
|
482
|
+
# Check and add Peak Shaving settings added in (ETU fw 22)
|
|
483
|
+
try:
|
|
484
|
+
await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47589, 6))
|
|
480
485
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
|
|
486
|
+
except RequestRejectedException as ex:
|
|
487
|
+
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
488
|
+
logger.debug("Cannot read PeakShaving setting, disabling it.")
|
|
489
|
+
self._has_peak_shaving = False
|
|
481
490
|
|
|
482
491
|
async def read_runtime_data(self) -> Dict[str, Any]:
|
|
483
492
|
response = await self._read_from_socket(self._READ_RUNNING_DATA)
|
|
@@ -541,6 +550,9 @@ class ET(Inverter):
|
|
|
541
550
|
setting = self._settings.get(setting_id)
|
|
542
551
|
if not setting:
|
|
543
552
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
553
|
+
return await self._read_setting(setting)
|
|
554
|
+
|
|
555
|
+
async def _read_setting(self, setting: Sensor) -> Any:
|
|
544
556
|
count = (setting.size_ + (setting.size_ % 2)) // 2
|
|
545
557
|
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
|
|
546
558
|
return setting.read_value(response)
|
|
@@ -549,6 +561,9 @@ class ET(Inverter):
|
|
|
549
561
|
setting = self._settings.get(setting_id)
|
|
550
562
|
if not setting:
|
|
551
563
|
raise ValueError(f'Unknown setting "{setting_id}"')
|
|
564
|
+
await self._write_setting(setting, value)
|
|
565
|
+
|
|
566
|
+
async def _write_setting(self, setting: Sensor, value: Any):
|
|
552
567
|
if setting.size_ == 1:
|
|
553
568
|
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
554
569
|
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
|
|
@@ -581,7 +596,7 @@ class ET(Inverter):
|
|
|
581
596
|
|
|
582
597
|
async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
|
|
583
598
|
result = [e for e in OperationMode]
|
|
584
|
-
if not self.
|
|
599
|
+
if not self._has_peak_shaving:
|
|
585
600
|
result.remove(OperationMode.PEAK_SHAVING)
|
|
586
601
|
if not include_emulated:
|
|
587
602
|
result.remove(OperationMode.ECO_CHARGE)
|
|
@@ -626,7 +641,8 @@ class ET(Inverter):
|
|
|
626
641
|
raise ValueError()
|
|
627
642
|
if eco_mode_soc < 0 or eco_mode_soc > 100:
|
|
628
643
|
raise ValueError()
|
|
629
|
-
eco_mode: EcoMode = self.
|
|
644
|
+
eco_mode: EcoMode | Sensor = self._settings.get('eco_mode_1')
|
|
645
|
+
await self._read_setting(eco_mode)
|
|
630
646
|
if operation_mode == OperationMode.ECO_CHARGE:
|
|
631
647
|
await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc))
|
|
632
648
|
else:
|
|
@@ -641,7 +657,7 @@ class ET(Inverter):
|
|
|
641
657
|
return 100 - await self.read_setting('battery_discharge_depth')
|
|
642
658
|
|
|
643
659
|
async def set_ongrid_battery_dod(self, dod: int) -> None:
|
|
644
|
-
if 0 <= dod <=
|
|
660
|
+
if 0 <= dod <= 100:
|
|
645
661
|
await self.write_setting('battery_discharge_depth', 100 - dod)
|
|
646
662
|
|
|
647
663
|
def sensors(self) -> Tuple[Sensor, ...]:
|
|
@@ -663,11 +679,3 @@ class ET(Inverter):
|
|
|
663
679
|
async def _set_offline(self, mode: bool) -> None:
|
|
664
680
|
value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
|
|
665
681
|
await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, 0xb997, value))
|
|
666
|
-
|
|
667
|
-
def _convert_eco_mode(self, sensor: Sensor) -> Sensor | EcoMode:
|
|
668
|
-
if EcoModeV1 == type(sensor) and self._supports_eco_mode_v2():
|
|
669
|
-
return cast(EcoModeV1, sensor).as_eco_mode_v2()
|
|
670
|
-
elif EcoModeV2 == type(sensor) and not self._supports_eco_mode_v2():
|
|
671
|
-
return cast(EcoModeV2, sensor).as_eco_mode_v1()
|
|
672
|
-
else:
|
|
673
|
-
return sensor
|
goodwe/protocol.py
CHANGED
|
@@ -85,7 +85,7 @@ class UdpInverterProtocol(asyncio.DatagramProtocol):
|
|
|
85
85
|
class ProtocolResponse:
|
|
86
86
|
"""Definition of response to protocol command"""
|
|
87
87
|
|
|
88
|
-
def __init__(self, raw_data: bytes, command: ProtocolCommand):
|
|
88
|
+
def __init__(self, raw_data: bytes, command: Optional[ProtocolCommand]):
|
|
89
89
|
self.raw_data: bytes = raw_data
|
|
90
90
|
self.command: ProtocolCommand = command
|
|
91
91
|
self._bytes: io.BytesIO = io.BytesIO(self.response_data())
|
|
@@ -116,6 +116,15 @@ class ProtocolCommand:
|
|
|
116
116
|
self.request: bytes = request
|
|
117
117
|
self.validator: Callable[[bytes], bool] = validator
|
|
118
118
|
|
|
119
|
+
def __eq__(self, other):
|
|
120
|
+
if not isinstance(other, ProtocolCommand):
|
|
121
|
+
# don't attempt to compare against unrelated types
|
|
122
|
+
return NotImplemented
|
|
123
|
+
return self.request == other.request
|
|
124
|
+
|
|
125
|
+
def __hash__(self):
|
|
126
|
+
return hash(self.request)
|
|
127
|
+
|
|
119
128
|
def __repr__(self):
|
|
120
129
|
return self.request.hex()
|
|
121
130
|
|
|
@@ -276,6 +285,7 @@ class ModbusProtocolCommand(ProtocolCommand):
|
|
|
276
285
|
lambda x: validate_modbus_response(x, cmd, offset, value),
|
|
277
286
|
)
|
|
278
287
|
self.first_address: int = offset
|
|
288
|
+
self.value = value
|
|
279
289
|
|
|
280
290
|
def trim_response(self, raw_response: bytes):
|
|
281
291
|
"""Trim raw response from header and checksum data"""
|
|
@@ -296,6 +306,12 @@ class ModbusReadCommand(ModbusProtocolCommand):
|
|
|
296
306
|
create_modbus_request(comm_addr, MODBUS_READ_CMD, offset, count),
|
|
297
307
|
MODBUS_READ_CMD, offset, count)
|
|
298
308
|
|
|
309
|
+
def __repr__(self):
|
|
310
|
+
if self.value > 1:
|
|
311
|
+
return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
|
|
312
|
+
else:
|
|
313
|
+
return f'READ register {self.first_address} ({self.request.hex()})'
|
|
314
|
+
|
|
299
315
|
|
|
300
316
|
class ModbusWriteCommand(ModbusProtocolCommand):
|
|
301
317
|
"""
|
|
@@ -307,6 +323,9 @@ class ModbusWriteCommand(ModbusProtocolCommand):
|
|
|
307
323
|
create_modbus_request(comm_addr, MODBUS_WRITE_CMD, register, value),
|
|
308
324
|
MODBUS_WRITE_CMD, register, value)
|
|
309
325
|
|
|
326
|
+
def __repr__(self):
|
|
327
|
+
return f'WRITE {self.value} to register {self.first_address} ({self.request.hex()})'
|
|
328
|
+
|
|
310
329
|
|
|
311
330
|
class ModbusWriteMultiCommand(ModbusProtocolCommand):
|
|
312
331
|
"""
|
goodwe/sensor.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from enum import IntEnum
|
|
5
6
|
from struct import unpack
|
|
6
7
|
from typing import Any, Callable, Optional
|
|
7
8
|
|
|
@@ -10,10 +11,79 @@ from .inverter import Sensor, SensorKind
|
|
|
10
11
|
from .protocol import ProtocolResponse
|
|
11
12
|
|
|
12
13
|
DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
|
14
|
+
MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ScheduleType(IntEnum):
|
|
18
|
+
ECO_MODE = 0,
|
|
19
|
+
DRY_CONTACT_LOAD = 1,
|
|
20
|
+
DRY_CONTACT_SMART_LOAD = 2,
|
|
21
|
+
PEAK_SHAVING = 3,
|
|
22
|
+
BACKUP_MODE = 4,
|
|
23
|
+
SMART_CHARGE_MODE = 5,
|
|
24
|
+
ECO_MODE_745 = 6
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def detect_schedule_type(cls, value: int) -> ScheduleType:
|
|
28
|
+
"""Detect schedule type from its on/off value"""
|
|
29
|
+
if value in (0, -1, 85):
|
|
30
|
+
return ScheduleType.ECO_MODE
|
|
31
|
+
elif value in (1, -2):
|
|
32
|
+
return ScheduleType.DRY_CONTACT_LOAD
|
|
33
|
+
elif value in (2, -3):
|
|
34
|
+
return ScheduleType.DRY_CONTACT_SMART_LOAD
|
|
35
|
+
elif value in (3, -4):
|
|
36
|
+
return ScheduleType.PEAK_SHAVING
|
|
37
|
+
elif value in (4, -5):
|
|
38
|
+
return ScheduleType.BACKUP_MODE
|
|
39
|
+
elif value in (5, -6):
|
|
40
|
+
return ScheduleType.SMART_CHARGE_MODE
|
|
41
|
+
elif value in (6, -7):
|
|
42
|
+
return ScheduleType.ECO_MODE_745
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"{value}: on_off value {value} out of range.")
|
|
45
|
+
|
|
46
|
+
def power_unit(self):
|
|
47
|
+
"""Return unit of power parameter"""
|
|
48
|
+
if self == ScheduleType.PEAK_SHAVING:
|
|
49
|
+
return "W"
|
|
50
|
+
else:
|
|
51
|
+
return "%"
|
|
52
|
+
|
|
53
|
+
def decode_power(self, value: int) -> int:
|
|
54
|
+
"""Decode human readable value of power parameter"""
|
|
55
|
+
if self == ScheduleType.ECO_MODE:
|
|
56
|
+
return value
|
|
57
|
+
elif self == ScheduleType.PEAK_SHAVING:
|
|
58
|
+
return value * 10
|
|
59
|
+
if self == ScheduleType.ECO_MODE_745:
|
|
60
|
+
return int(value / 10)
|
|
61
|
+
else:
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
def encode_power(self, value: int) -> int:
|
|
65
|
+
"""Encode human readable value of power parameter"""
|
|
66
|
+
if self == ScheduleType.ECO_MODE:
|
|
67
|
+
return value
|
|
68
|
+
elif self == ScheduleType.PEAK_SHAVING:
|
|
69
|
+
return int(value / 10)
|
|
70
|
+
if self == ScheduleType.ECO_MODE_745:
|
|
71
|
+
return value * 10
|
|
72
|
+
else:
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
def is_in_range(self, value: int) -> bool:
|
|
76
|
+
"""Check if the value fits in allowed values range"""
|
|
77
|
+
if self == ScheduleType.ECO_MODE:
|
|
78
|
+
return -100 <= value <= 100
|
|
79
|
+
if self == ScheduleType.ECO_MODE_745:
|
|
80
|
+
return -1000 <= value <= 1000
|
|
81
|
+
else:
|
|
82
|
+
return True
|
|
13
83
|
|
|
14
84
|
|
|
15
85
|
class Voltage(Sensor):
|
|
16
|
-
"""Sensor representing voltage [V] value encoded in 2 bytes"""
|
|
86
|
+
"""Sensor representing voltage [V] value encoded in 2 (unsigned) bytes"""
|
|
17
87
|
|
|
18
88
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
19
89
|
super().__init__(id_, offset, name, 2, "V", kind)
|
|
@@ -26,7 +96,7 @@ class Voltage(Sensor):
|
|
|
26
96
|
|
|
27
97
|
|
|
28
98
|
class Current(Sensor):
|
|
29
|
-
"""Sensor representing current [A] value encoded in 2 bytes"""
|
|
99
|
+
"""Sensor representing current [A] value encoded in 2 (unsigned) bytes"""
|
|
30
100
|
|
|
31
101
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
32
102
|
super().__init__(id_, offset, name, 2, "A", kind)
|
|
@@ -38,6 +108,19 @@ class Current(Sensor):
|
|
|
38
108
|
return encode_current(value)
|
|
39
109
|
|
|
40
110
|
|
|
111
|
+
class CurrentS(Sensor):
|
|
112
|
+
"""Sensor representing current [A] value encoded in 2 (signed) bytes"""
|
|
113
|
+
|
|
114
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
115
|
+
super().__init__(id_, offset, name, 2, "A", kind)
|
|
116
|
+
|
|
117
|
+
def read_value(self, data: ProtocolResponse):
|
|
118
|
+
return read_current_signed(data)
|
|
119
|
+
|
|
120
|
+
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
121
|
+
return encode_current_signed(value)
|
|
122
|
+
|
|
123
|
+
|
|
41
124
|
class Frequency(Sensor):
|
|
42
125
|
"""Sensor representing frequency [Hz] value encoded in 2 bytes"""
|
|
43
126
|
|
|
@@ -49,7 +132,7 @@ class Frequency(Sensor):
|
|
|
49
132
|
|
|
50
133
|
|
|
51
134
|
class Power(Sensor):
|
|
52
|
-
"""Sensor representing power [W] value encoded in 2 bytes"""
|
|
135
|
+
"""Sensor representing power [W] value encoded in 2 (unsigned) bytes"""
|
|
53
136
|
|
|
54
137
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
55
138
|
super().__init__(id_, offset, name, 2, "W", kind)
|
|
@@ -58,8 +141,18 @@ class Power(Sensor):
|
|
|
58
141
|
return read_bytes2(data)
|
|
59
142
|
|
|
60
143
|
|
|
144
|
+
class PowerS(Sensor):
|
|
145
|
+
"""Sensor representing power [W] value encoded in 2 (signed) bytes"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
148
|
+
super().__init__(id_, offset, name, 2, "W", kind)
|
|
149
|
+
|
|
150
|
+
def read_value(self, data: ProtocolResponse):
|
|
151
|
+
return read_bytes2_signed(data)
|
|
152
|
+
|
|
153
|
+
|
|
61
154
|
class Power4(Sensor):
|
|
62
|
-
"""Sensor representing power [W] value encoded in 4 bytes"""
|
|
155
|
+
"""Sensor representing power [W] value encoded in 4 (unsigned) bytes"""
|
|
63
156
|
|
|
64
157
|
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
65
158
|
super().__init__(id_, offset, name, 4, "W", kind)
|
|
@@ -68,6 +161,16 @@ class Power4(Sensor):
|
|
|
68
161
|
return read_bytes4(data)
|
|
69
162
|
|
|
70
163
|
|
|
164
|
+
class Power4S(Sensor):
|
|
165
|
+
"""Sensor representing power [W] value encoded in 4 (signed) bytes"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
|
|
168
|
+
super().__init__(id_, offset, name, 4, "W", kind)
|
|
169
|
+
|
|
170
|
+
def read_value(self, data: ProtocolResponse):
|
|
171
|
+
return read_bytes4_signed(data)
|
|
172
|
+
|
|
173
|
+
|
|
71
174
|
class Energy(Sensor):
|
|
72
175
|
"""Sensor representing energy [kWh] value encoded in 2 bytes"""
|
|
73
176
|
|
|
@@ -76,10 +179,7 @@ class Energy(Sensor):
|
|
|
76
179
|
|
|
77
180
|
def read_value(self, data: ProtocolResponse):
|
|
78
181
|
value = read_bytes2(data)
|
|
79
|
-
|
|
80
|
-
return None
|
|
81
|
-
else:
|
|
82
|
-
return float(value) / 10
|
|
182
|
+
return float(value) / 10
|
|
83
183
|
|
|
84
184
|
|
|
85
185
|
class Energy4(Sensor):
|
|
@@ -90,10 +190,7 @@ class Energy4(Sensor):
|
|
|
90
190
|
|
|
91
191
|
def read_value(self, data: ProtocolResponse):
|
|
92
192
|
value = read_bytes4(data)
|
|
93
|
-
|
|
94
|
-
return None
|
|
95
|
-
else:
|
|
96
|
-
return float(value) / 10
|
|
193
|
+
return float(value) / 10
|
|
97
194
|
|
|
98
195
|
|
|
99
196
|
class Apparent(Sensor):
|
|
@@ -103,7 +200,7 @@ class Apparent(Sensor):
|
|
|
103
200
|
super().__init__(id_, offset, name, 2, "VA", kind)
|
|
104
201
|
|
|
105
202
|
def read_value(self, data: ProtocolResponse):
|
|
106
|
-
return
|
|
203
|
+
return read_bytes2_signed(data)
|
|
107
204
|
|
|
108
205
|
|
|
109
206
|
class Apparent4(Sensor):
|
|
@@ -113,7 +210,7 @@ class Apparent4(Sensor):
|
|
|
113
210
|
super().__init__(id_, offset, name, 2, "VA", kind)
|
|
114
211
|
|
|
115
212
|
def read_value(self, data: ProtocolResponse):
|
|
116
|
-
return
|
|
213
|
+
return read_bytes4_signed(data)
|
|
117
214
|
|
|
118
215
|
|
|
119
216
|
class Reactive(Sensor):
|
|
@@ -123,7 +220,7 @@ class Reactive(Sensor):
|
|
|
123
220
|
super().__init__(id_, offset, name, 2, "var", kind)
|
|
124
221
|
|
|
125
222
|
def read_value(self, data: ProtocolResponse):
|
|
126
|
-
return
|
|
223
|
+
return read_bytes2_signed(data)
|
|
127
224
|
|
|
128
225
|
|
|
129
226
|
class Reactive4(Sensor):
|
|
@@ -133,7 +230,7 @@ class Reactive4(Sensor):
|
|
|
133
230
|
super().__init__(id_, offset, name, 2, "var", kind)
|
|
134
231
|
|
|
135
232
|
def read_value(self, data: ProtocolResponse):
|
|
136
|
-
return
|
|
233
|
+
return read_bytes4_signed(data)
|
|
137
234
|
|
|
138
235
|
|
|
139
236
|
class Temp(Sensor):
|
|
@@ -207,7 +304,7 @@ class Integer(Sensor):
|
|
|
207
304
|
super().__init__(id_, offset, name, 2, unit, kind)
|
|
208
305
|
|
|
209
306
|
def read_value(self, data: ProtocolResponse):
|
|
210
|
-
return
|
|
307
|
+
return read_bytes2_signed(data)
|
|
211
308
|
|
|
212
309
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
213
310
|
return int.to_bytes(int(value), length=2, byteorder="big", signed=True)
|
|
@@ -220,7 +317,7 @@ class Long(Sensor):
|
|
|
220
317
|
super().__init__(id_, offset, name, 4, unit, kind)
|
|
221
318
|
|
|
222
319
|
def read_value(self, data: ProtocolResponse):
|
|
223
|
-
return
|
|
320
|
+
return read_bytes4_signed(data)
|
|
224
321
|
|
|
225
322
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
226
323
|
return int.to_bytes(int(value), length=4, byteorder="big", signed=True)
|
|
@@ -320,7 +417,7 @@ class EnumBitmap4(Sensor):
|
|
|
320
417
|
raise NotImplementedError()
|
|
321
418
|
|
|
322
419
|
def read(self, data: ProtocolResponse):
|
|
323
|
-
bits =
|
|
420
|
+
bits = read_bytes4_signed(data, self.offset)
|
|
324
421
|
return decode_bitmap(bits if bits != -1 else 0, self._labels)
|
|
325
422
|
|
|
326
423
|
|
|
@@ -413,7 +510,7 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
413
510
|
self.end_m = read_byte(data)
|
|
414
511
|
if self.end_m < 0 or self.end_m > 59:
|
|
415
512
|
raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.")
|
|
416
|
-
self.power =
|
|
513
|
+
self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
|
|
417
514
|
if self.power < -100 or self.power > 100:
|
|
418
515
|
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
|
|
419
516
|
self.on_off = read_byte(data)
|
|
@@ -479,10 +576,10 @@ class EcoModeV1(Sensor, EcoMode):
|
|
|
479
576
|
return result
|
|
480
577
|
|
|
481
578
|
|
|
482
|
-
class
|
|
483
|
-
"""Sensor representing
|
|
579
|
+
class Schedule(Sensor, EcoMode):
|
|
580
|
+
"""Sensor representing Schedule Group encoded in 12 bytes"""
|
|
484
581
|
|
|
485
|
-
def __init__(self, id_: str, offset: int, name: str):
|
|
582
|
+
def __init__(self, id_: str, offset: int, name: str, schedule_type: ScheduleType = ScheduleType.ECO_MODE):
|
|
486
583
|
super().__init__(id_, offset, name, 12, "", SensorKind.BAT)
|
|
487
584
|
self.start_h: int | None = None
|
|
488
585
|
self.start_m: int | None = None
|
|
@@ -493,39 +590,43 @@ class EcoModeV2(Sensor, EcoMode):
|
|
|
493
590
|
self.days: str | None = None
|
|
494
591
|
self.power: int | None = None
|
|
495
592
|
self.soc: int | None = None
|
|
496
|
-
|
|
593
|
+
self.month_bits: int | None = None
|
|
594
|
+
self.months: str | None = None
|
|
595
|
+
self.schedule_type: ScheduleType = schedule_type
|
|
497
596
|
|
|
498
597
|
def __str__(self):
|
|
499
598
|
return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} " \
|
|
500
|
-
f"{self.
|
|
501
|
-
f"{
|
|
599
|
+
f"{self.months + ' ' if self.months else ''}" \
|
|
600
|
+
f"{self.schedule_type.decode_power(self.power)}{self.schedule_type.power_unit()} (SoC {self.soc}%) " \
|
|
601
|
+
f"{'On' if -10 < self.on_off < 0 else 'Off' if 10 > self.on_off >= 0 else 'Unset'}"
|
|
502
602
|
|
|
503
603
|
def read_value(self, data: ProtocolResponse):
|
|
504
604
|
self.start_h = read_byte(data)
|
|
505
|
-
if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48:
|
|
605
|
+
if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48 and self.start_h != -1:
|
|
506
606
|
raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.")
|
|
507
607
|
self.start_m = read_byte(data)
|
|
508
|
-
if self.start_m < 0 or self.start_m > 59:
|
|
608
|
+
if (self.start_m < 0 or self.start_m > 59) and self.start_m != -1:
|
|
509
609
|
raise ValueError(f"{self.id_}: start_m value {self.start_m} out of range.")
|
|
510
610
|
self.end_h = read_byte(data)
|
|
511
|
-
if (self.end_h < 0 or self.end_h > 23) and self.end_h != 48:
|
|
611
|
+
if (self.end_h < 0 or self.end_h > 23) and self.end_h != 48 and self.end_h != -1:
|
|
512
612
|
raise ValueError(f"{self.id_}: end_h value {self.end_h} out of range.")
|
|
513
613
|
self.end_m = read_byte(data)
|
|
514
|
-
if self.end_m < 0 or self.end_m > 59:
|
|
614
|
+
if (self.end_m < 0 or self.end_m > 59) and self.end_m != -1:
|
|
515
615
|
raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.")
|
|
516
616
|
self.on_off = read_byte(data)
|
|
517
|
-
|
|
518
|
-
raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
|
|
617
|
+
self.schedule_type = ScheduleType.detect_schedule_type(self.on_off)
|
|
519
618
|
self.day_bits = read_byte(data)
|
|
520
619
|
self.days = decode_day_of_week(self.day_bits)
|
|
521
620
|
if self.day_bits < 0:
|
|
522
621
|
raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
|
|
523
|
-
self.power =
|
|
524
|
-
if self.
|
|
622
|
+
self.power = read_bytes2_signed(data) # negative=charge, positive=discharge
|
|
623
|
+
if not self.schedule_type.is_in_range(self.power):
|
|
525
624
|
raise ValueError(f"{self.id_}: power value {self.power} out of range.")
|
|
526
|
-
self.soc =
|
|
625
|
+
self.soc = read_bytes2_signed(data)
|
|
527
626
|
if self.soc < 0 or self.soc > 100:
|
|
528
627
|
raise ValueError(f"{self.id_}: SoC value {self.soc} out of range.")
|
|
628
|
+
self.month_bits = read_bytes2_signed(data)
|
|
629
|
+
self.months = decode_months(self.month_bits)
|
|
529
630
|
return self
|
|
530
631
|
|
|
531
632
|
def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
|
|
@@ -538,15 +639,24 @@ class EcoModeV2(Sensor, EcoMode):
|
|
|
538
639
|
def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes:
|
|
539
640
|
"""Answer bytes representing all the time enabled charging eco mode group"""
|
|
540
641
|
return bytes.fromhex(
|
|
541
|
-
"
|
|
642
|
+
"0000173b{:02x}7f{:04x}{:04x}{:04x}".format(
|
|
643
|
+
255 - self.schedule_type,
|
|
644
|
+
(-1 * abs(self.schedule_type.encode_power(eco_mode_power))) & (2 ** 16 - 1),
|
|
645
|
+
eco_mode_soc,
|
|
646
|
+
0 if self.schedule_type != ScheduleType.ECO_MODE_745 else 0x0fff))
|
|
542
647
|
|
|
543
648
|
def encode_discharge(self, eco_mode_power: int) -> bytes:
|
|
544
649
|
"""Answer bytes representing all the time enabled discharging eco mode group"""
|
|
545
|
-
return bytes.fromhex("
|
|
650
|
+
return bytes.fromhex("0000173b{:02x}7f{:04x}0064{:04x}".format(
|
|
651
|
+
255 - self.schedule_type,
|
|
652
|
+
abs(self.schedule_type.encode_power(eco_mode_power)),
|
|
653
|
+
0 if self.schedule_type != ScheduleType.ECO_MODE_745 else 0x0fff))
|
|
546
654
|
|
|
547
655
|
def encode_off(self) -> bytes:
|
|
548
|
-
"""Answer bytes representing empty and disabled
|
|
549
|
-
return bytes.fromhex("
|
|
656
|
+
"""Answer bytes representing empty and disabled schedule group"""
|
|
657
|
+
return bytes.fromhex("30003000{:02x}00{:04x}00640000".format(
|
|
658
|
+
self.schedule_type.value,
|
|
659
|
+
self.schedule_type.encode_power(100)))
|
|
550
660
|
|
|
551
661
|
def is_eco_charge_mode(self) -> bool:
|
|
552
662
|
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
|
|
@@ -554,9 +664,10 @@ class EcoModeV2(Sensor, EcoMode):
|
|
|
554
664
|
and self.start_m == 0 \
|
|
555
665
|
and self.end_h == 23 \
|
|
556
666
|
and self.end_m == 59 \
|
|
557
|
-
and self.on_off == -1 \
|
|
667
|
+
and self.on_off == (-1 - self.schedule_type) \
|
|
558
668
|
and self.day_bits == 127 \
|
|
559
|
-
and self.power < 0
|
|
669
|
+
and self.power < 0 \
|
|
670
|
+
and (self.month_bits == 0 or self.month_bits == 0x0fff)
|
|
560
671
|
|
|
561
672
|
def is_eco_discharge_mode(self) -> bool:
|
|
562
673
|
"""Answer if it represents the emulated 24/7 fulltime discharge mode"""
|
|
@@ -564,9 +675,10 @@ class EcoModeV2(Sensor, EcoMode):
|
|
|
564
675
|
and self.start_m == 0 \
|
|
565
676
|
and self.end_h == 23 \
|
|
566
677
|
and self.end_m == 59 \
|
|
567
|
-
and self.on_off == -1 \
|
|
678
|
+
and self.on_off == (-1 - self.schedule_type) \
|
|
568
679
|
and self.day_bits == 127 \
|
|
569
|
-
and self.power > 0
|
|
680
|
+
and self.power > 0 \
|
|
681
|
+
and (self.month_bits == 0 or self.month_bits == 0x0fff)
|
|
570
682
|
|
|
571
683
|
def as_eco_mode_v1(self) -> EcoModeV1:
|
|
572
684
|
"""Convert V2 to V1 EcoMode"""
|
|
@@ -582,65 +694,18 @@ class EcoModeV2(Sensor, EcoMode):
|
|
|
582
694
|
return result
|
|
583
695
|
|
|
584
696
|
|
|
585
|
-
class
|
|
586
|
-
"""Sensor representing
|
|
697
|
+
class EcoModeV2(Schedule):
|
|
698
|
+
"""Sensor representing Eco Mode Group encoded in 12 bytes"""
|
|
587
699
|
|
|
588
700
|
def __init__(self, id_: str, offset: int, name: str):
|
|
589
|
-
super().__init__(id_, offset, name,
|
|
590
|
-
self.start_h: int | None = None
|
|
591
|
-
self.start_m: int | None = None
|
|
592
|
-
self.end_h: int | None = None
|
|
593
|
-
self.end_m: int | None = None
|
|
594
|
-
self.on_off: int | None = None
|
|
595
|
-
self.day_bits: int | None = None
|
|
596
|
-
self.days: str | None = None
|
|
597
|
-
self.import_power: float | None = None
|
|
598
|
-
self.soc: int | None = None
|
|
599
|
-
# 2 bytes padding 0000
|
|
701
|
+
super().__init__(id_, offset, name, ScheduleType.ECO_MODE)
|
|
600
702
|
|
|
601
|
-
def __str__(self):
|
|
602
|
-
return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} " \
|
|
603
|
-
f"{self.import_power}kW (SoC {self.soc}%) " \
|
|
604
|
-
f"{'On' if self.on_off == -4 else 'Off' if self.on_off == 3 else 'Unset'}"
|
|
605
|
-
|
|
606
|
-
def read_value(self, data: ProtocolResponse):
|
|
607
|
-
self.start_h = read_byte(data)
|
|
608
|
-
if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48:
|
|
609
|
-
raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.")
|
|
610
|
-
self.start_m = read_byte(data)
|
|
611
|
-
if self.start_m < 0 or self.start_m > 59:
|
|
612
|
-
raise ValueError(f"{self.id_}: start_m value {self.start_m} out of range.")
|
|
613
|
-
self.end_h = read_byte(data)
|
|
614
|
-
if (self.end_h < 0 or self.end_h > 23) and self.end_h != 48:
|
|
615
|
-
raise ValueError(f"{self.id_}: end_h value {self.end_h} out of range.")
|
|
616
|
-
self.end_m = read_byte(data)
|
|
617
|
-
if self.end_m < 0 or self.end_m > 59:
|
|
618
|
-
raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.")
|
|
619
|
-
self.on_off = read_byte(data)
|
|
620
|
-
if self.on_off not in (-4, 3, 85):
|
|
621
|
-
raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.")
|
|
622
|
-
self.day_bits = read_byte(data)
|
|
623
|
-
self.days = decode_day_of_week(self.day_bits)
|
|
624
|
-
if self.day_bits < 0:
|
|
625
|
-
raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.")
|
|
626
|
-
self.import_power = read_decimal2(data, 100)
|
|
627
|
-
if self.import_power < 0 or self.import_power > 500:
|
|
628
|
-
raise ValueError(f"{self.id_}: import_power value {self.import_power} out of range.")
|
|
629
|
-
self.soc = read_bytes2(data)
|
|
630
|
-
if self.soc < 0 or self.soc > 100:
|
|
631
|
-
raise ValueError(f"{self.id_}: soc value {self.soc} out of range.")
|
|
632
|
-
return self
|
|
633
703
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
# try to read_value to check if values are valid
|
|
637
|
-
if self.read_value(ProtocolResponse(value, None)):
|
|
638
|
-
return value
|
|
639
|
-
raise ValueError
|
|
704
|
+
class PeakShavingMode(Schedule):
|
|
705
|
+
"""Sensor representing Peak Shaving Mode encoded in 12 bytes"""
|
|
640
706
|
|
|
641
|
-
def
|
|
642
|
-
|
|
643
|
-
return bytes.fromhex("300030000000006400640000")
|
|
707
|
+
def __init__(self, id_: str, offset: int, name: str):
|
|
708
|
+
super().__init__(id_, offset, name, ScheduleType.PEAK_SHAVING)
|
|
644
709
|
|
|
645
710
|
|
|
646
711
|
class Calculated(Sensor):
|
|
@@ -666,6 +731,14 @@ def read_byte(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
666
731
|
|
|
667
732
|
|
|
668
733
|
def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
734
|
+
"""Retrieve 2 byte (unsigned int) value from buffer"""
|
|
735
|
+
if offset is not None:
|
|
736
|
+
buffer.seek(offset)
|
|
737
|
+
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
738
|
+
return value if value != 0xffff else 0
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def read_bytes2_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
669
742
|
"""Retrieve 2 byte (signed int) value from buffer"""
|
|
670
743
|
if offset is not None:
|
|
671
744
|
buffer.seek(offset)
|
|
@@ -673,6 +746,14 @@ def read_bytes2(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
673
746
|
|
|
674
747
|
|
|
675
748
|
def read_bytes4(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
749
|
+
"""Retrieve 4 byte (unsigned int) value from buffer"""
|
|
750
|
+
if offset is not None:
|
|
751
|
+
buffer.seek(offset)
|
|
752
|
+
value = int.from_bytes(buffer.read(4), byteorder="big", signed=False)
|
|
753
|
+
return value if value != 0xffffffff else 0
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
676
757
|
"""Retrieve 4 byte (signed int) value from buffer"""
|
|
677
758
|
if offset is not None:
|
|
678
759
|
buffer.seek(offset)
|
|
@@ -698,20 +779,28 @@ def read_float4(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
|
698
779
|
|
|
699
780
|
|
|
700
781
|
def read_voltage(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
701
|
-
"""Retrieve voltage [V] value (2 bytes) from buffer"""
|
|
782
|
+
"""Retrieve voltage [V] value (2 unsigned bytes) from buffer"""
|
|
702
783
|
if offset is not None:
|
|
703
784
|
buffer.seek(offset)
|
|
704
|
-
value = int.from_bytes(buffer.read(2), byteorder="big", signed=
|
|
705
|
-
return float(value) / 10
|
|
785
|
+
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
786
|
+
return float(value) / 10 if value != 0xffff else 0
|
|
706
787
|
|
|
707
788
|
|
|
708
789
|
def encode_voltage(value: Any) -> bytes:
|
|
709
|
-
"""Encode voltage value to raw (2 bytes) payload"""
|
|
710
|
-
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=
|
|
790
|
+
"""Encode voltage value to raw (2 unsigned bytes) payload"""
|
|
791
|
+
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=False)
|
|
711
792
|
|
|
712
793
|
|
|
713
794
|
def read_current(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
714
|
-
"""Retrieve current [A] value (2 bytes) from buffer"""
|
|
795
|
+
"""Retrieve current [A] value (2 unsigned bytes) from buffer"""
|
|
796
|
+
if offset is not None:
|
|
797
|
+
buffer.seek(offset)
|
|
798
|
+
value = int.from_bytes(buffer.read(2), byteorder="big", signed=False)
|
|
799
|
+
return float(value) / 10 if value != 0xffff else 0
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def read_current_signed(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
803
|
+
"""Retrieve current [A] value (2 signed bytes) from buffer"""
|
|
715
804
|
if offset is not None:
|
|
716
805
|
buffer.seek(offset)
|
|
717
806
|
value = int.from_bytes(buffer.read(2), byteorder="big", signed=True)
|
|
@@ -719,7 +808,12 @@ def read_current(buffer: ProtocolResponse, offset: int = None) -> float:
|
|
|
719
808
|
|
|
720
809
|
|
|
721
810
|
def encode_current(value: Any) -> bytes:
|
|
722
|
-
"""Encode current value to raw (2 bytes) payload"""
|
|
811
|
+
"""Encode current value to raw (2 unsigned bytes) payload"""
|
|
812
|
+
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=False)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def encode_current_signed(value: Any) -> bytes:
|
|
816
|
+
"""Encode current value to raw (2 signed bytes) payload"""
|
|
723
817
|
return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=True)
|
|
724
818
|
|
|
725
819
|
|
|
@@ -771,7 +865,7 @@ def encode_datetime(value: Any) -> bytes:
|
|
|
771
865
|
|
|
772
866
|
def read_grid_mode(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
773
867
|
"""Retrieve 'grid mode' sign value from buffer"""
|
|
774
|
-
value =
|
|
868
|
+
value = read_bytes2_signed(buffer, offset)
|
|
775
869
|
if value < -90:
|
|
776
870
|
return 2
|
|
777
871
|
elif value >= 90:
|
|
@@ -807,3 +901,18 @@ def decode_day_of_week(data: int) -> str:
|
|
|
807
901
|
days += daynames[0]
|
|
808
902
|
daynames.pop(0)
|
|
809
903
|
return days
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def decode_months(data: int) -> str | None:
|
|
907
|
+
if data == 0 or data == 0x0fff:
|
|
908
|
+
return None
|
|
909
|
+
bits = bin(data)[2:]
|
|
910
|
+
monthnames = list(MONTH_NAMES)
|
|
911
|
+
months = ""
|
|
912
|
+
for each in bits[::-1]:
|
|
913
|
+
if each == '1':
|
|
914
|
+
if len(months) > 0:
|
|
915
|
+
months += ","
|
|
916
|
+
months += monthnames[0]
|
|
917
|
+
monthnames.pop(0)
|
|
918
|
+
return months
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
goodwe/__init__.py,sha256=PInrrZEpTmMOQKk494vIz8EKSaw_qLBNz-6t9eLIUcg,5642
|
|
2
|
+
goodwe/const.py,sha256=Nw-nd4UJuqUOLfbmOrxTHEdS1AuaTDSpZzQqR6tBb8w,7912
|
|
3
|
+
goodwe/dt.py,sha256=bI53MVdZjtxTYU2qJLO8icsvF6UiXrkgH95V3iUwXT0,10581
|
|
4
|
+
goodwe/es.py,sha256=d0RyW70dnxaMNVZzSxE7eEoW2dMDzQQxA4MlSADOyuo,22417
|
|
5
|
+
goodwe/et.py,sha256=zQUZAhaC4HUV_0hzF6M85ffXasBBKSbpNkT_x37EZjw,38443
|
|
6
|
+
goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
|
|
7
|
+
goodwe/inverter.py,sha256=HvVtFBz5Zvl1je1V2AuLqpoB1vsur_gDHu-6SjNbJ8o,10323
|
|
8
|
+
goodwe/modbus.py,sha256=ZPib-zKnOVE5zc0RNnhlf0w_26QBees1ScWGo6bAj0o,4685
|
|
9
|
+
goodwe/model.py,sha256=Yy662c6VpuLozxo1Q7Llw1g34UhT5qJd9_rITIjmEd4,1523
|
|
10
|
+
goodwe/protocol.py,sha256=pUkXTP2DqpKXGO7rbRfHq1x82Y1QM6OiRVx8cAtS0sM,13162
|
|
11
|
+
goodwe/sensor.py,sha256=M8v3flB4V7VxY91G1m2a1tdaD5j_lqtVzkxxrcTFHmE,34696
|
|
12
|
+
goodwe-0.3.2.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
|
|
13
|
+
goodwe-0.3.2.dist-info/METADATA,sha256=izdqayJnYLUwJt9DPS2oUrgMUxGzNNIDMmokDNb3RNs,3050
|
|
14
|
+
goodwe-0.3.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
15
|
+
goodwe-0.3.2.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
|
|
16
|
+
goodwe-0.3.2.dist-info/RECORD,,
|
goodwe-0.3.0.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
goodwe/__init__.py,sha256=PInrrZEpTmMOQKk494vIz8EKSaw_qLBNz-6t9eLIUcg,5642
|
|
2
|
-
goodwe/const.py,sha256=Nw-nd4UJuqUOLfbmOrxTHEdS1AuaTDSpZzQqR6tBb8w,7912
|
|
3
|
-
goodwe/dt.py,sha256=bI53MVdZjtxTYU2qJLO8icsvF6UiXrkgH95V3iUwXT0,10581
|
|
4
|
-
goodwe/es.py,sha256=dZ_pGb6tI_PNJUm4gWlS0voN22kc37yDq4aNeqeteCM,22649
|
|
5
|
-
goodwe/et.py,sha256=_v7dvRQ18JBBOgwbTn_T2qQIHOO8TjLcavmmJWJEzq8,38032
|
|
6
|
-
goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
|
|
7
|
-
goodwe/inverter.py,sha256=HvVtFBz5Zvl1je1V2AuLqpoB1vsur_gDHu-6SjNbJ8o,10323
|
|
8
|
-
goodwe/modbus.py,sha256=ZPib-zKnOVE5zc0RNnhlf0w_26QBees1ScWGo6bAj0o,4685
|
|
9
|
-
goodwe/model.py,sha256=Yy662c6VpuLozxo1Q7Llw1g34UhT5qJd9_rITIjmEd4,1523
|
|
10
|
-
goodwe/protocol.py,sha256=P0KYAVoLNkE1Ws70F4AWqqzWbzGA11P8VOYj1wDXDoo,12480
|
|
11
|
-
goodwe/sensor.py,sha256=pGbeZFmvvFcaSKgxgIhUIp-k1wW1jF0RVDfrkMGEdJY,31027
|
|
12
|
-
goodwe-0.3.0.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
|
|
13
|
-
goodwe-0.3.0.dist-info/METADATA,sha256=R4H86lZ6qBXAoBqEhFYTom36JrEAEJfHmSISe8kUkRA,3050
|
|
14
|
-
goodwe-0.3.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
15
|
-
goodwe-0.3.0.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
|
|
16
|
-
goodwe-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|