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 CHANGED
@@ -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
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, 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
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
- if value == -1:
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
- if value == -1:
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 read_bytes2(data)
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 read_bytes4(data)
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 read_bytes2(data)
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 read_bytes4(data)
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 read_bytes2(data)
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 read_bytes4(data)
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 = read_bytes4(data, self.offset)
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 = read_bytes2(data) # negative=charge, positive=discharge
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 EcoModeV2(Sensor, EcoMode):
483
- """Sensor representing Eco Mode Battery Power Group encoded in 12 bytes"""
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
- # 2 bytes padding 0000
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.power}% (SoC {self.soc}%) " \
501
- f"{'On' if self.on_off == -1 else 'Off' if self.on_off == 0 else 'Unset'}"
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
- if self.on_off not in (0, -1, 85):
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 = read_bytes2(data) # negative=charge, positive=discharge
524
- if self.power < -100 or self.power > 100:
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 = read_bytes2(data)
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
- "0000173bff7f{:04x}{:04x}0000".format((-1 * abs(eco_mode_power)) & (2 ** 16 - 1), eco_mode_soc))
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("0000173bff7f{:04x}00640000".format(abs(eco_mode_power)))
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 eco mode group"""
549
- return bytes.fromhex("300030000000006400640000")
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 PeakShavingMode(Sensor):
586
- """Sensor representing Peak Shaving Mode encoded in 12 bytes"""
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, 12, "", SensorKind.BAT)
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
- def encode_value(self, value: Any, register_value: bytes = None) -> bytes:
635
- if isinstance(value, bytes) and len(value) == 12:
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 encode_off(self) -> bytes:
642
- """Answer bytes representing empty and disabled eco mode group"""
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=True)
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=True)
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 = read_bytes2(buffer, offset)
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
@@ -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
@@ -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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,