goodwe 0.3.0__tar.gz → 0.3.2__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.
Files changed (28) hide show
  1. {goodwe-0.3.0/goodwe.egg-info → goodwe-0.3.2}/PKG-INFO +1 -1
  2. goodwe-0.3.2/VERSION +1 -0
  3. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/es.py +40 -41
  4. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/et.py +54 -46
  5. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/protocol.py +20 -1
  6. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/sensor.py +214 -105
  7. {goodwe-0.3.0 → goodwe-0.3.2/goodwe.egg-info}/PKG-INFO +1 -1
  8. {goodwe-0.3.0 → goodwe-0.3.2}/tests/test_dt.py +9 -9
  9. {goodwe-0.3.0 → goodwe-0.3.2}/tests/test_et.py +12 -5
  10. {goodwe-0.3.0 → goodwe-0.3.2}/tests/test_sensor.py +92 -4
  11. goodwe-0.3.0/VERSION +0 -1
  12. {goodwe-0.3.0 → goodwe-0.3.2}/LICENSE +0 -0
  13. {goodwe-0.3.0 → goodwe-0.3.2}/README.md +0 -0
  14. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/__init__.py +0 -0
  15. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/const.py +0 -0
  16. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/dt.py +0 -0
  17. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/exceptions.py +0 -0
  18. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/inverter.py +0 -0
  19. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/modbus.py +0 -0
  20. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe/model.py +0 -0
  21. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe.egg-info/SOURCES.txt +0 -0
  22. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe.egg-info/dependency_links.txt +0 -0
  23. {goodwe-0.3.0 → goodwe-0.3.2}/goodwe.egg-info/top_level.txt +0 -0
  24. {goodwe-0.3.0 → goodwe-0.3.2}/pyproject.toml +0 -0
  25. {goodwe-0.3.0 → goodwe-0.3.2}/setup.cfg +0 -0
  26. {goodwe-0.3.0 → goodwe-0.3.2}/tests/test_es.py +0 -0
  27. {goodwe-0.3.0 → goodwe-0.3.2}/tests/test_modbus.py +0 -0
  28. {goodwe-0.3.0 → goodwe-0.3.2}/tests/test_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Read data from GoodWe inverter via local network
5
5
  Home-page: https://github.com/marcelblijleven/goodwe
6
6
  Author: Martin Letenay, Marcel Blijleven
goodwe-0.3.2/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.2
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Tuple, cast
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(read_bytes2(data, 38)) * (-1 if read_byte(data, 80) == 2 else 1),
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
- Power("total_power", 75, "Total Power", Kind.AC), # modbus 0x52c
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(read_bytes2(data, 38)) * (-1 if read_byte(data, 80) == 2 else 1)),
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
- count = (setting.size_ + (setting.size_ % 2)) // 2
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
- if setting.size_ == 1:
244
- # modbus can address/store only 16 bit values, read the other 8 bytes
245
- if self._is_modbus_setting(setting):
246
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
247
- raw_value = setting.encode_value(value, response.response_data()[0:2])
248
- else:
249
- response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
250
- raw_value = setting.encode_value(value, response.response_data()[2:4])
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
- raw_value = setting.encode_value(value)
253
- if len(raw_value) <= 2:
254
- value = int.from_bytes(raw_value, byteorder="big", signed=True)
255
- if self._is_modbus_setting(setting):
256
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
257
- else:
258
- await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
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
- if self._is_modbus_setting(setting):
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._convert_eco_mode(EcoModeV2("", 0, ""))
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 <= 89:
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 EcoModeV2 == type(sensor) or sensor.offset > 30000
438
+ return sensor.offset > 30000
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Tuple, cast
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
- Power("pgrid", 35125, "On-grid L1 Power", Kind.AC),
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
- Power("pgrid2", 35130, "On-grid L2 Power", Kind.AC),
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
- Power("pgrid3", 35135, "On-grid L3 Power", Kind.AC),
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
- Power("total_inverter_power", 35138, "Total Power", Kind.AC),
69
+ PowerS("total_inverter_power", 35138, "Total Power", Kind.AC),
70
70
  # 35139 reserved
71
- Power("active_power", 35140, "Active Power", Kind.GRID),
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
- Power("backup_p1", 35150, "Back-up L1 Power", Kind.UPS),
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
- Power("backup_p2", 35156, "Back-up L2 Power", Kind.UPS),
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
- Power("backup_p3", 35162, "Back-up L3 Power", Kind.UPS),
99
+ PowerS("backup_p3", 35162, "Back-up L3 Power", Kind.UPS),
100
100
  # 35163 reserved
101
- Power("load_p1", 35164, "Load L1", Kind.AC),
101
+ PowerS("load_p1", 35164, "Load L1", Kind.AC),
102
102
  # 35165 reserved
103
- Power("load_p2", 35166, "Load L2", Kind.AC),
103
+ PowerS("load_p2", 35166, "Load L2", Kind.AC),
104
104
  # 35167 reserved
105
- Power("load_p3", 35168, "Load L3", Kind.AC),
105
+ PowerS("load_p3", 35168, "Load L3", Kind.AC),
106
106
  # 35169 reserved
107
- Power("backup_ptotal", 35170, "Back-up Load", Kind.UPS),
107
+ PowerS("backup_ptotal", 35170, "Back-up Load", Kind.UPS),
108
108
  # 35171 reserved
109
- Power("load_ptotal", 35172, "Load", Kind.AC),
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
- Current("ibattery1", 35181, "Battery Current", Kind.BAT),
119
- Power4("pbattery1", 35182, "Battery Power", Kind.BAT),
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
- read_bytes4(data, 35182) -
153
- read_bytes2(data, 35140),
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
- Power("active_power1", 36005, "Active Power L1", Kind.GRID),
230
- Power("active_power2", 36006, "Active Power L2", Kind.GRID),
231
- Power("active_power3", 36007, "Active Power L3", Kind.GRID),
232
- Power("active_power_total", 36008, "Active Power Total", Kind.GRID),
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
- Power4("meter_active_power1", 36019, "Meter Active Power L1", Kind.GRID),
242
- Power4("meter_active_power2", 36021, "Meter Active Power L2", Kind.GRID),
243
- Power4("meter_active_power3", 36023, "Meter Active Power L3", Kind.GRID),
244
- Power4("meter_active_power_total", 36025, "Meter Active Power Total", Kind.GRID),
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
- Power4("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
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
- if self.arm_version >= 19 or 'ETU' not in self.serial_number:
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
- if self.arm_version >= 22 or 'ETU' not in self.serial_number:
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._supports_peak_shaving():
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._convert_eco_mode(EcoModeV2("", 0, ""))
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 <= 90:
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
@@ -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
  """