goodwe 0.4.0__py3-none-any.whl → 0.4.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/dt.py CHANGED
@@ -1,15 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  from typing import Tuple
4
5
 
5
- from .exceptions import InverterError
6
+ from .exceptions import InverterError, RequestRejectedException
6
7
  from .inverter import Inverter
7
8
  from .inverter import OperationMode
8
9
  from .inverter import SensorKind as Kind
10
+ from .modbus import ILLEGAL_DATA_ADDRESS
9
11
  from .model import is_3_mppt, is_single_phase
10
12
  from .protocol import ProtocolCommand
11
13
  from .sensor import *
12
14
 
15
+ logger = logging.getLogger(__name__)
16
+
13
17
 
14
18
  class DT(Inverter):
15
19
  """Class representing inverter of DT/MS/D-NS/XS or GE's GEP(PSB/PSC) families"""
@@ -123,10 +127,7 @@ class DT(Inverter):
123
127
  )
124
128
 
125
129
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
126
- super().__init__(host, port, comm_addr, timeout, retries)
127
- if not self.comm_addr:
128
- # Set the default inverter address
129
- self.comm_addr = 0x7f
130
+ super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
130
131
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
131
132
  self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
132
133
  self._sensors = self.__all_sensors
@@ -177,17 +178,43 @@ class DT(Inverter):
177
178
 
178
179
  async def read_setting(self, setting_id: str) -> Any:
179
180
  setting = self._settings.get(setting_id)
180
- if not setting:
181
- raise ValueError(f'Unknown setting "{setting_id}"')
182
- count = (setting.size_ + (setting.size_ % 2)) // 2
183
- response = await self._read_from_socket(self._read_command(setting.offset, count))
184
- return setting.read_value(response)
181
+ if setting:
182
+ return await self._read_setting(setting)
183
+ else:
184
+ if setting_id.startswith("modbus"):
185
+ response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
186
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
187
+ else:
188
+ raise ValueError(f'Unknown setting "{setting_id}"')
189
+
190
+ async def _read_setting(self, setting: Sensor) -> Any:
191
+ try:
192
+ count = (setting.size_ + (setting.size_ % 2)) // 2
193
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
194
+ return setting.read_value(response)
195
+ except RequestRejectedException as ex:
196
+ if ex.message == ILLEGAL_DATA_ADDRESS:
197
+ logger.debug("Unsupported setting %s", setting.id_)
198
+ self._settings.pop(setting.id_, None)
199
+ return None
185
200
 
186
201
  async def write_setting(self, setting_id: str, value: Any):
187
202
  setting = self._settings.get(setting_id)
188
- if not setting:
189
- raise ValueError(f'Unknown setting "{setting_id}"')
190
- raw_value = setting.encode_value(value)
203
+ if setting:
204
+ await self._write_setting(setting, value)
205
+ else:
206
+ if setting_id.startswith("modbus"):
207
+ await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
208
+ else:
209
+ raise ValueError(f'Unknown setting "{setting_id}"')
210
+
211
+ async def _write_setting(self, setting: Sensor, value: Any):
212
+ if setting.size_ == 1:
213
+ # modbus can address/store only 16 bit values, read the other 8 bytes
214
+ response = await self._read_from_socket(self._read_command(setting.offset, 1))
215
+ raw_value = setting.encode_value(value, response.response_data()[0:2])
216
+ else:
217
+ raw_value = setting.encode_value(value)
191
218
  if len(raw_value) <= 2:
192
219
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
193
220
  await self._read_from_socket(self._write_command(setting.offset, value))
goodwe/es.py CHANGED
@@ -168,10 +168,7 @@ class ES(Inverter):
168
168
  )
169
169
 
170
170
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
171
- super().__init__(host, port, comm_addr, timeout, retries)
172
- if not self.comm_addr:
173
- # Set the default inverter address
174
- self.comm_addr = 0xf7
171
+ super().__init__(host, port, comm_addr if comm_addr else 0xf7, timeout, retries)
175
172
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
176
173
 
177
174
  def _supports_eco_mode_v2(self) -> bool:
@@ -220,6 +217,9 @@ class ES(Inverter):
220
217
  if not setting:
221
218
  raise ValueError(f'Unknown setting "{setting_id}"')
222
219
  return await self._read_setting(setting)
220
+ elif setting_id.startswith("modbus"):
221
+ response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
222
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
223
223
  else:
224
224
  all_settings = await self.read_settings_data()
225
225
  return all_settings.get(setting_id)
@@ -238,6 +238,8 @@ class ES(Inverter):
238
238
  await self._read_from_socket(
239
239
  Aa55ProtocolCommand("030206" + Timestamp("time", 0, "").encode_value(value).hex(), "0382")
240
240
  )
241
+ elif setting_id.startswith("modbus"):
242
+ await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
241
243
  else:
242
244
  setting: Sensor | None = self._settings.get(setting_id)
243
245
  if not setting:
@@ -249,10 +251,9 @@ class ES(Inverter):
249
251
  # modbus can address/store only 16 bit values, read the other 8 bytes
250
252
  if self._is_modbus_setting(setting):
251
253
  response = await self._read_from_socket(self._read_command(setting.offset, 1))
252
- raw_value = setting.encode_value(value, response.response_data()[0:2])
253
254
  else:
254
255
  response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
255
- raw_value = setting.encode_value(value, response.response_data()[2:4])
256
+ raw_value = setting.encode_value(value, response.response_data()[0:2])
256
257
  else:
257
258
  raw_value = setting.encode_value(value)
258
259
  if len(raw_value) <= 2:
goodwe/et.py CHANGED
@@ -3,10 +3,11 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import Tuple
5
5
 
6
- from .exceptions import RequestRejectedException
6
+ from .exceptions import RequestFailedException, RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
10
+ from .modbus import ILLEGAL_DATA_ADDRESS
10
11
  from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
11
12
  from .protocol import ProtocolCommand
12
13
  from .sensor import *
@@ -331,7 +332,7 @@ class ET(Inverter):
331
332
  # Modbus registers of inverter settings, offsets are modbus register addresses
332
333
  __all_settings: Tuple[Sensor, ...] = (
333
334
  Integer("comm_address", 45127, "Communication Address", ""),
334
-
335
+ Integer("modbus_baud_rate", 45132, "Modbus Baud rate", ""),
335
336
  Timestamp("time", 45200, "Inverter time"),
336
337
 
337
338
  Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
@@ -371,6 +372,51 @@ class ET(Inverter):
371
372
  ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
372
373
  EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
373
374
  ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),
375
+
376
+ # Direct BMS communication for EMS Control
377
+ Integer("bms_version", 47900, "BMS Version"),
378
+ Integer("bms_bat_modules", 47901, "BMS Battery Modules"),
379
+ # Real time read from BMS
380
+ Voltage("bms_bat_charge_v_max", 47902, "BMS Battery Charge Voltage (max)", Kind.BMS),
381
+ Current("bms_bat_charge_i_max", 47903, "BMS Battery Charge Current (max)", Kind.BMS),
382
+ Voltage("bms_bat_discharge_v_min", 47904, "BMS min. Battery Discharge Voltage (min)", Kind.BMS),
383
+ Current("bms_bat_discharge_i_max", 47905, "BMS max. Battery Discharge Current (max)", Kind.BMS),
384
+ Voltage("bms_bat_voltage", 47906, "BMS Battery Voltage", Kind.BMS),
385
+ Current("bms_bat_current", 47907, "BMS Battery Current", Kind.BMS),
386
+ #
387
+ Integer("bms_bat_soc", 47908, "BMS Battery State of Charge", "%", Kind.BMS),
388
+ Integer("bms_bat_soh", 47909, "BMS Battery State of Health", "%", Kind.BMS),
389
+ Temp("bms_bat_temperature", 47910, "BMS Battery Temperature", Kind.BMS),
390
+ Long("bms_bat_warning-code", 47911, "BMS Battery Warning Code"),
391
+ # Reserved
392
+ Long("bms_bat_alarm-code", 47913, "BMS Battery Alarm Code"),
393
+ Integer("bms_status", 47915, "BMS Status"),
394
+ Integer("bms_comm_loss_disable", 47916, "BMS Communication Loss Disable"),
395
+ # RW settings of BMS voltage rate
396
+ Integer("bms_battery_string_rate_v", 47917, "BMS Battery String Rate Voltage"),
397
+
398
+ # Direct BMS communication for EMS Control
399
+ Integer("bms2_version", 47918, "BMS2 Version"),
400
+ Integer("bms2_bat_modules", 47919, "BMS2 Battery Modules"),
401
+ # Real time read from BMS
402
+ Voltage("bms2_bat_charge_v_max", 47920, "BMS2 Battery Charge Voltage (max)", Kind.BMS),
403
+ Current("bms2_bat_charge_i_max", 47921, "BMS2 Battery Charge Current (max)", Kind.BMS),
404
+ Voltage("bms2_bat_discharge_v_min", 47922, "BMS2 min. Battery Discharge Voltage (min)", Kind.BMS),
405
+ Current("bms2_bat_discharge_i_max", 47923, "BMS2 max. Battery Discharge Current (max)", Kind.BMS),
406
+ Voltage("bms2_bat_voltage", 47924, "BMS2 Battery Voltage", Kind.BMS),
407
+ Current("bms2_bat_current", 47925, "BMS2 Battery Current", Kind.BMS),
408
+ #
409
+ Integer("bms2_bat_soc", 47926, "BMS2 Battery State of Charge", "%", Kind.BMS),
410
+ Integer("bms2_bat_soh", 47927, "BMS2 Battery State of Health", "%", Kind.BMS),
411
+ Temp("bms2_bat_temperature", 47928, "BMS2 Battery Temperature", Kind.BMS),
412
+ Long("bms2_bat_warning-code", 47929, "BMS2 Battery Warning Code"),
413
+ # Reserved
414
+ Long("bms2_bat_alarm-code", 47931, "BMS2 Battery Alarm Code"),
415
+ Integer("bms2_status", 47933, "BMS2 Status"),
416
+ Integer("bms2_comm_loss_disable", 47934, "BMS2 Communication Loss Disable"),
417
+ # RW settings of BMS voltage rate
418
+ Integer("bms2_battery_string_rate_v", 47935, "BMS2 Battery String Rate Voltage"),
419
+
374
420
  )
375
421
 
376
422
  # Settings added in ARM firmware 19
@@ -389,6 +435,7 @@ class ET(Inverter):
389
435
  Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
390
436
  Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
391
437
  Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
438
+ Integer("hardware_feed_power", 47599, "Hardware Feed Power"),
392
439
 
393
440
  Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
394
441
  )
@@ -410,10 +457,7 @@ class ET(Inverter):
410
457
  )
411
458
 
412
459
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
413
- super().__init__(host, port, comm_addr, timeout, retries)
414
- if not self.comm_addr:
415
- # Set the default inverter address
416
- self.comm_addr = 0xf7
460
+ super().__init__(host, port, comm_addr if comm_addr else 0xf7, timeout, retries)
417
461
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
418
462
  self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
419
463
  self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
@@ -447,19 +491,19 @@ class ET(Inverter):
447
491
  async def read_device_info(self):
448
492
  response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
449
493
  response = response.response_data()
450
- # Modbus registers from offset (35000)
494
+ # Modbus registers from 35000 - 35032
451
495
  self.modbus_version = read_unsigned_int(response, 0)
452
496
  self.rated_power = read_unsigned_int(response, 2)
453
497
  self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
454
- self.serial_number = self._decode(response[6:22])
455
- self.model_name = self._decode(response[22:32])
456
- self.dsp1_version = read_unsigned_int(response, 32)
457
- self.dsp2_version = read_unsigned_int(response, 34)
458
- self.dsp_svn_version = read_unsigned_int(response, 36)
459
- self.arm_version = read_unsigned_int(response, 38)
460
- self.arm_svn_version = read_unsigned_int(response, 40)
461
- self.firmware = self._decode(response[42:54])
462
- self.arm_firmware = self._decode(response[54:66])
498
+ self.serial_number = self._decode(response[6:22]) # 35003 - 350010
499
+ self.model_name = self._decode(response[22:32]) # 35011 - 35015
500
+ self.dsp1_version = read_unsigned_int(response, 32) # 35016
501
+ self.dsp2_version = read_unsigned_int(response, 34) # 35017
502
+ self.dsp_svn_version = read_unsigned_int(response, 36) # 35018
503
+ self.arm_version = read_unsigned_int(response, 38) # 35019
504
+ self.arm_svn_version = read_unsigned_int(response, 40) # 35020
505
+ self.firmware = self._decode(response[42:54]) # 35021 - 35027
506
+ self.arm_firmware = self._decode(response[54:66]) # 35027 - 35032
463
507
 
464
508
  if not is_4_mppt(self) and self.rated_power < 15000:
465
509
  # This inverter does not have 4 MPPTs or PV strings
@@ -485,18 +529,24 @@ class ET(Inverter):
485
529
  await self._read_from_socket(self._read_command(47547, 6))
486
530
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
487
531
  except RequestRejectedException as ex:
488
- if ex.message == 'ILLEGAL DATA ADDRESS':
489
- logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
532
+ if ex.message == ILLEGAL_DATA_ADDRESS:
533
+ logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
490
534
  self._has_eco_mode_v2 = False
535
+ except RequestFailedException:
536
+ logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
537
+ self._has_eco_mode_v2 = False
491
538
 
492
539
  # Check and add Peak Shaving settings added in (ETU fw 22)
493
540
  try:
494
541
  await self._read_from_socket(self._read_command(47589, 6))
495
542
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
496
543
  except RequestRejectedException as ex:
497
- if ex.message == 'ILLEGAL DATA ADDRESS':
498
- logger.debug("Cannot read PeakShaving setting, disabling it.")
544
+ if ex.message == ILLEGAL_DATA_ADDRESS:
545
+ logger.debug("PeakShaving setting not supported, disabling it.")
499
546
  self._has_peak_shaving = False
547
+ except RequestFailedException:
548
+ logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
549
+ self._has_peak_shaving = False
500
550
 
501
551
  async def read_runtime_data(self) -> Dict[str, Any]:
502
552
  response = await self._read_from_socket(self._READ_RUNNING_DATA)
@@ -508,8 +558,8 @@ class ET(Inverter):
508
558
  response = await self._read_from_socket(self._READ_BATTERY_INFO)
509
559
  data.update(self._map_response(response, self._sensors_battery))
510
560
  except RequestRejectedException as ex:
511
- if ex.message == 'ILLEGAL DATA ADDRESS':
512
- logger.warning("Cannot read battery values, disabling further attempts.")
561
+ if ex.message == ILLEGAL_DATA_ADDRESS:
562
+ logger.info("Battery values not supported, disabling further attempts.")
513
563
  self._has_battery = False
514
564
  else:
515
565
  raise ex
@@ -519,8 +569,8 @@ class ET(Inverter):
519
569
  data.update(
520
570
  self._map_response(response, self._sensors_battery2))
521
571
  except RequestRejectedException as ex:
522
- if ex.message == 'ILLEGAL DATA ADDRESS':
523
- logger.warning("Cannot read battery 2 values, disabling further attempts.")
572
+ if ex.message == ILLEGAL_DATA_ADDRESS:
573
+ logger.info("Battery 2 values not supported, disabling further attempts.")
524
574
  self._has_battery2 = False
525
575
  else:
526
576
  raise ex
@@ -530,8 +580,8 @@ class ET(Inverter):
530
580
  response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
531
581
  data.update(self._map_response(response, self._sensors_meter))
532
582
  except RequestRejectedException as ex:
533
- if ex.message == 'ILLEGAL DATA ADDRESS':
534
- logger.warning("Cannot read extended meter values, disabling further attempts.")
583
+ if ex.message == ILLEGAL_DATA_ADDRESS:
584
+ logger.info("Extended meter values not supported, disabling further attempts.")
535
585
  self._has_meter_extended = False
536
586
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
537
587
  response = await self._read_from_socket(self._READ_METER_DATA)
@@ -548,8 +598,8 @@ class ET(Inverter):
548
598
  response = await self._read_from_socket(self._READ_MPPT_DATA)
549
599
  data.update(self._map_response(response, self._sensors_mppt))
550
600
  except RequestRejectedException as ex:
551
- if ex.message == 'ILLEGAL DATA ADDRESS':
552
- logger.warning("Cannot read MPPT values, disabling further attempts.")
601
+ if ex.message == ILLEGAL_DATA_ADDRESS:
602
+ logger.info("MPPT values not supported, disabling further attempts.")
553
603
  self._has_mppt = False
554
604
  else:
555
605
  raise ex
@@ -558,20 +608,35 @@ class ET(Inverter):
558
608
 
559
609
  async def read_setting(self, setting_id: str) -> Any:
560
610
  setting = self._settings.get(setting_id)
561
- if not setting:
562
- raise ValueError(f'Unknown setting "{setting_id}"')
563
- return await self._read_setting(setting)
611
+ if setting:
612
+ return await self._read_setting(setting)
613
+ else:
614
+ if setting_id.startswith("modbus"):
615
+ response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
616
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
617
+ else:
618
+ raise ValueError(f'Unknown setting "{setting_id}"')
564
619
 
565
620
  async def _read_setting(self, setting: Sensor) -> Any:
566
- count = (setting.size_ + (setting.size_ % 2)) // 2
567
- response = await self._read_from_socket(self._read_command(setting.offset, count))
568
- return setting.read_value(response)
621
+ try:
622
+ count = (setting.size_ + (setting.size_ % 2)) // 2
623
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
624
+ return setting.read_value(response)
625
+ except RequestRejectedException as ex:
626
+ if ex.message == ILLEGAL_DATA_ADDRESS:
627
+ logger.debug("Unsupported setting %s", setting.id_)
628
+ self._settings.pop(setting.id_, None)
629
+ return None
569
630
 
570
631
  async def write_setting(self, setting_id: str, value: Any):
571
632
  setting = self._settings.get(setting_id)
572
- if not setting:
573
- raise ValueError(f'Unknown setting "{setting_id}"')
574
- await self._write_setting(setting, value)
633
+ if setting:
634
+ await self._write_setting(setting, value)
635
+ else:
636
+ if setting_id.startswith("modbus"):
637
+ await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
638
+ else:
639
+ raise ValueError(f'Unknown setting "{setting_id}"')
575
640
 
576
641
  async def _write_setting(self, setting: Sensor, value: Any):
577
642
  if setting.size_ == 1:
@@ -592,7 +657,7 @@ class ET(Inverter):
592
657
  try:
593
658
  value = await self.read_setting(setting.id_)
594
659
  data[setting.id_] = value
595
- except ValueError:
660
+ except (ValueError, RequestFailedException):
596
661
  logger.exception("Error reading setting %s.", setting.id_)
597
662
  data[setting.id_] = None
598
663
  return data
goodwe/exceptions.py CHANGED
@@ -29,5 +29,19 @@ class RequestRejectedException(InverterError):
29
29
  self.message: str = message
30
30
 
31
31
 
32
+ class PartialResponseException(InverterError):
33
+ """
34
+ Indicates the received response data are incomplete and is probably fragmented to multiple packets.
35
+
36
+ Attributes:
37
+ length -- received data length
38
+ expected -- expected data lenght
39
+ """
40
+
41
+ def __init__(self, lenght: int, expected: int):
42
+ self.length: int = lenght
43
+ self.expected: int = expected
44
+
45
+
32
46
  class MaxRetriesException(InverterError):
33
47
  """Indicates the maximum number of retries has been reached"""
goodwe/inverter.py CHANGED
@@ -22,6 +22,7 @@ class SensorKind(Enum):
22
22
  UPS - inverter ups/eps/backup output (e.g. ac voltage of backup/off-grid connected output)
23
23
  BAT - battery (e.g. dc voltage of connected battery pack)
24
24
  GRID - power grid/smart meter (e.g. active power exported to grid)
25
+ BMS - BMS direct data (e.g. dc voltage of)
25
26
  """
26
27
 
27
28
  PV = 1
@@ -29,6 +30,7 @@ class SensorKind(Enum):
29
30
  UPS = 3
30
31
  BAT = 4
31
32
  GRID = 5
33
+ BMS = 6
32
34
 
33
35
 
34
36
  @dataclass
@@ -87,10 +89,9 @@ class Inverter(ABC):
87
89
  """
88
90
 
89
91
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
90
- self._protocol: InverterProtocol = self._create_protocol(host, port, timeout, retries)
92
+ self._protocol: InverterProtocol = self._create_protocol(host, port, comm_addr, timeout, retries)
91
93
  self._consecutive_failures_count: int = 0
92
-
93
- self.comm_addr: int = comm_addr
94
+ self.keep_alive: bool = True
94
95
 
95
96
  self.model_name: str | None = None
96
97
  self.serial_number: str | None = None
@@ -107,15 +108,15 @@ class Inverter(ABC):
107
108
 
108
109
  def _read_command(self, offset: int, count: int) -> ProtocolCommand:
109
110
  """Create read protocol command."""
110
- return self._protocol.read_command(self.comm_addr, offset, count)
111
+ return self._protocol.read_command(offset, count)
111
112
 
112
113
  def _write_command(self, register: int, value: int) -> ProtocolCommand:
113
114
  """Create write protocol command."""
114
- return self._protocol.write_command(self.comm_addr, register, value)
115
+ return self._protocol.write_command(register, value)
115
116
 
116
117
  def _write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
117
118
  """Create write multiple protocol command."""
118
- return self._protocol.write_multi_command(self.comm_addr, offset, values)
119
+ return self._protocol.write_multi_command(offset, values)
119
120
 
120
121
  async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
121
122
  try:
@@ -129,6 +130,9 @@ class Inverter(ABC):
129
130
  except RequestFailedException as ex:
130
131
  self._consecutive_failures_count += 1
131
132
  raise RequestFailedException(ex.message, self._consecutive_failures_count) from None
133
+ finally:
134
+ if not self.keep_alive:
135
+ self._protocol.close_transport()
132
136
 
133
137
  @abstractmethod
134
138
  async def read_device_info(self):
@@ -268,11 +272,11 @@ class Inverter(ABC):
268
272
  raise NotImplementedError()
269
273
 
270
274
  @staticmethod
271
- def _create_protocol(host: str, port: int, timeout: int, retries: int) -> InverterProtocol:
275
+ def _create_protocol(host: str, port: int, comm_addr: int, timeout: int, retries: int) -> InverterProtocol:
272
276
  if port == 502:
273
- return TcpInverterProtocol(host, port, timeout, retries)
277
+ return TcpInverterProtocol(host, port, comm_addr, timeout, retries)
274
278
  else:
275
- return UdpInverterProtocol(host, port, timeout, retries)
279
+ return UdpInverterProtocol(host, port, comm_addr, timeout, retries)
276
280
 
277
281
  @staticmethod
278
282
  def _map_response(response: ProtocolResponse, sensors: Tuple[Sensor, ...]) -> Dict[str, Any]:
goodwe/modbus.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from typing import Union
3
3
 
4
- from .exceptions import RequestRejectedException
4
+ from .exceptions import PartialResponseException, RequestRejectedException
5
5
 
6
6
  logger = logging.getLogger(__name__)
7
7
 
@@ -9,9 +9,11 @@ MODBUS_READ_CMD: int = 0x3
9
9
  MODBUS_WRITE_CMD: int = 0x6
10
10
  MODBUS_WRITE_MULTI_CMD: int = 0x10
11
11
 
12
+ ILLEGAL_DATA_ADDRESS: str = 'ILLEGAL DATA ADDRESS'
13
+
12
14
  FAILURE_CODES = {
13
15
  1: "ILLEGAL FUNCTION",
14
- 2: "ILLEGAL DATA ADDRESS",
16
+ 2: ILLEGAL_DATA_ADDRESS,
15
17
  3: "ILLEGAL DATA VALUE",
16
18
  4: "SLAVE DEVICE FAILURE",
17
19
  5: "ACKNOWLEDGE",
@@ -176,8 +178,7 @@ def validate_modbus_rtu_response(data: bytes, cmd: int, offset: int, value: int)
176
178
  return False
177
179
  expected_length = data[4] + 7
178
180
  if len(data) < expected_length:
179
- logger.debug("Response is too short: %d, expected %d.", len(data), expected_length)
180
- return False
181
+ raise PartialResponseException(len(data), expected_length)
181
182
  elif data[3] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
182
183
  if len(data) < 10:
183
184
  logger.debug("Response has unexpected length: %d, expected %d.", len(data), 10)
@@ -220,18 +221,18 @@ def validate_modbus_tcp_response(data: bytes, cmd: int, offset: int, value: int)
220
221
  if len(data) <= 8:
221
222
  logger.debug("Response is too short.")
222
223
  return False
224
+ expected_length = int.from_bytes(data[4:6], byteorder='big', signed=False) + 6
225
+ if len(data) < expected_length:
226
+ raise PartialResponseException(len(data), expected_length)
227
+
223
228
  if data[7] == MODBUS_READ_CMD:
224
229
  if data[8] != value * 2:
225
230
  logger.debug("Response has unexpected length: %d, expected %d.", data[8], value * 2)
226
231
  return False
227
- expected_length = data[8] + 9
228
- if len(data) < expected_length:
229
- logger.debug("Response is too short: %d, expected %d.", len(data), expected_length)
230
- return False
231
232
  elif data[7] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
232
233
  if len(data) < 12:
233
234
  logger.debug("Response has unexpected length: %d, expected %d.", len(data), 14)
234
- return False
235
+ raise PartialResponseException(len(data), expected_length)
235
236
  response_offset = int.from_bytes(data[8:10], byteorder='big', signed=False)
236
237
  if response_offset != offset:
237
238
  logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset)
goodwe/protocol.py CHANGED
@@ -3,22 +3,35 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import io
5
5
  import logging
6
+ import platform
7
+ import socket
6
8
  from asyncio.futures import Future
7
9
  from typing import Tuple, Optional, Callable
8
10
 
9
- from .exceptions import MaxRetriesException, RequestFailedException, RequestRejectedException
11
+ from .exceptions import MaxRetriesException, PartialResponseException, RequestFailedException, RequestRejectedException
10
12
  from .modbus import create_modbus_rtu_request, create_modbus_rtu_multi_request, create_modbus_tcp_request, \
11
13
  create_modbus_tcp_multi_request, validate_modbus_rtu_response, validate_modbus_tcp_response, MODBUS_READ_CMD, \
12
14
  MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
18
+ _modbus_tcp_tx = 0
19
+
20
+
21
+ def _next_tx() -> bytes:
22
+ global _modbus_tcp_tx
23
+ _modbus_tcp_tx += 1
24
+ if _modbus_tcp_tx == 0xFFFF:
25
+ _modbus_tcp_tx = 1
26
+ return int.to_bytes(_modbus_tcp_tx, length=2, byteorder="big", signed=False)
27
+
16
28
 
17
29
  class InverterProtocol:
18
30
 
19
- def __init__(self, host: str, port: int, timeout: int, retries: int):
31
+ def __init__(self, host: str, port: int, comm_addr: int, timeout: int, retries: int):
20
32
  self._host: str = host
21
33
  self._port: int = port
34
+ self._comm_addr: int = comm_addr
22
35
  self._running_loop: asyncio.AbstractEventLoop | None = None
23
36
  self._lock: asyncio.Lock | None = None
24
37
  self._timer: asyncio.TimerHandle | None = None
@@ -27,6 +40,7 @@ class InverterProtocol:
27
40
  self.protocol: asyncio.Protocol | None = None
28
41
  self.response_future: Future | None = None
29
42
  self.command: ProtocolCommand | None = None
43
+ self._partial_data: bytes | None = None
30
44
 
31
45
  def _ensure_lock(self) -> asyncio.Lock:
32
46
  """Validate (or create) asyncio Lock.
@@ -43,45 +57,47 @@ class InverterProtocol:
43
57
  logger.debug("Creating lock instance for current event loop.")
44
58
  self._lock = asyncio.Lock()
45
59
  self._running_loop = asyncio.get_event_loop()
46
- self._close_transport()
60
+ self.close_transport()
47
61
  return self._lock
48
62
 
49
- def _close_transport(self) -> None:
63
+ def close_transport(self) -> None:
64
+ """Close the underlying transport/connection."""
50
65
  raise NotImplementedError()
51
66
 
52
67
  async def send_request(self, command: ProtocolCommand) -> Future:
68
+ """Convert command to request and send it to inverter."""
53
69
  raise NotImplementedError()
54
70
 
55
- def read_command(self, comm_addr: int, offset: int, count: int) -> ProtocolCommand:
71
+ def read_command(self, offset: int, count: int) -> ProtocolCommand:
56
72
  """Create read protocol command."""
57
73
  raise NotImplementedError()
58
74
 
59
- def write_command(self, comm_addr: int, register: int, value: int) -> ProtocolCommand:
75
+ def write_command(self, register: int, value: int) -> ProtocolCommand:
60
76
  """Create write protocol command."""
61
77
  raise NotImplementedError()
62
78
 
63
- def write_multi_command(self, comm_addr: int, offset: int, values: bytes) -> ProtocolCommand:
79
+ def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
64
80
  """Create write multiple protocol command."""
65
81
  raise NotImplementedError()
66
82
 
67
83
 
68
84
  class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
69
- def __init__(self, host: str, port: int, timeout: int = 1, retries: int = 3):
70
- super().__init__(host, port, timeout, retries)
85
+ def __init__(self, host: str, port: int, comm_addr: int, timeout: int = 1, retries: int = 3):
86
+ super().__init__(host, port, comm_addr, timeout, retries)
71
87
  self._transport: asyncio.transports.DatagramTransport | None = None
72
88
  self._retry: int = 0
73
89
 
74
- def read_command(self, comm_addr: int, offset: int, count: int) -> ProtocolCommand:
90
+ def read_command(self, offset: int, count: int) -> ProtocolCommand:
75
91
  """Create read protocol command."""
76
- return ModbusRtuReadCommand(comm_addr, offset, count)
92
+ return ModbusRtuReadCommand(self._comm_addr, offset, count)
77
93
 
78
- def write_command(self, comm_addr: int, register: int, value: int) -> ProtocolCommand:
94
+ def write_command(self, register: int, value: int) -> ProtocolCommand:
79
95
  """Create write protocol command."""
80
- return ModbusRtuWriteCommand(comm_addr, register, value)
96
+ return ModbusRtuWriteCommand(self._comm_addr, register, value)
81
97
 
82
- def write_multi_command(self, comm_addr: int, offset: int, values: bytes) -> ProtocolCommand:
98
+ def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
83
99
  """Create write multiple protocol command."""
84
- return ModbusRtuWriteMultiCommand(comm_addr, offset, values)
100
+ return ModbusRtuWriteMultiCommand(self._comm_addr, offset, values)
85
101
 
86
102
  async def _connect(self) -> None:
87
103
  if not self._transport or self._transport.is_closing():
@@ -100,7 +116,7 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
100
116
  logger.debug("Socket closed with error: %s.", exc)
101
117
  else:
102
118
  logger.debug("Socket closed.")
103
- self._close_transport()
119
+ self.close_transport()
104
120
 
105
121
  def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
106
122
  """On datagram received"""
@@ -108,22 +124,35 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
108
124
  self._timer.cancel()
109
125
  self._timer = None
110
126
  try:
127
+ if self._partial_data:
128
+ logger.debug("Received another response fragment: %s.", data.hex())
129
+ data = self._partial_data + data
111
130
  if self.command.validator(data):
112
- logger.debug("Received: %s", data.hex())
131
+ if self._partial_data:
132
+ logger.debug("Composed fragmented response: %s", data.hex())
133
+ else:
134
+ logger.debug("Received: %s", data.hex())
135
+ self._partial_data = None
113
136
  self.response_future.set_result(data)
114
137
  else:
115
138
  logger.debug("Received invalid response: %s", data.hex())
116
139
  asyncio.get_running_loop().call_soon(self._retry_mechanism)
140
+ except PartialResponseException:
141
+ logger.debug("Received response fragment: %s", data.hex())
142
+ self._partial_data = data
143
+ return
144
+ except asyncio.InvalidStateError:
145
+ logger.debug("Response already handled: %s", data.hex())
117
146
  except RequestRejectedException as ex:
118
147
  logger.debug("Received exception response: %s", data.hex())
119
148
  self.response_future.set_exception(ex)
120
- self._close_transport()
149
+ self.close_transport()
121
150
 
122
151
  def error_received(self, exc: Exception) -> None:
123
152
  """On error received"""
124
153
  logger.debug("Received error: %s", exc)
125
154
  self.response_future.set_exception(exc)
126
- self._close_transport()
155
+ self.close_transport()
127
156
 
128
157
  async def send_request(self, command: ProtocolCommand) -> Future:
129
158
  """Send message via transport"""
@@ -139,9 +168,12 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
139
168
  """Send message via transport"""
140
169
  self.command = command
141
170
  self.response_future = response_future
142
- logger.debug("Sending: %s%s", self.command,
143
- f' - retry #{self._retry}/{self.retries}' if self._retry > 0 else '')
144
- self._transport.sendto(self.command.request)
171
+ payload = command.request_bytes()
172
+ if self._retry > 0:
173
+ logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
174
+ else:
175
+ logger.debug("Sending: %s", self.command)
176
+ self._transport.sendto(payload)
145
177
  self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
146
178
 
147
179
  def _retry_mechanism(self) -> None:
@@ -156,9 +188,9 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
156
188
  else:
157
189
  logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
158
190
  self.response_future.set_exception(MaxRetriesException)
159
- self._close_transport()
191
+ self.close_transport()
160
192
 
161
- def _close_transport(self) -> None:
193
+ def close_transport(self) -> None:
162
194
  if self._transport:
163
195
  try:
164
196
  self._transport.close()
@@ -171,22 +203,22 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
171
203
 
172
204
 
173
205
  class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
174
- def __init__(self, host: str, port: int, timeout: int = 1, retries: int = 0):
175
- super().__init__(host, port, timeout, retries)
206
+ def __init__(self, host: str, port: int, comm_addr: int, timeout: int = 1, retries: int = 0):
207
+ super().__init__(host, port, comm_addr, timeout, retries)
176
208
  self._transport: asyncio.transports.Transport | None = None
177
209
  self._retry: int = 0
178
210
 
179
- def read_command(self, comm_addr: int, offset: int, count: int) -> ProtocolCommand:
211
+ def read_command(self, offset: int, count: int) -> ProtocolCommand:
180
212
  """Create read protocol command."""
181
- return ModbusTcpReadCommand(comm_addr, offset, count)
213
+ return ModbusTcpReadCommand(self._comm_addr, offset, count)
182
214
 
183
- def write_command(self, comm_addr: int, register: int, value: int) -> ProtocolCommand:
215
+ def write_command(self, register: int, value: int) -> ProtocolCommand:
184
216
  """Create write protocol command."""
185
- return ModbusTcpWriteCommand(comm_addr, register, value)
217
+ return ModbusTcpWriteCommand(self._comm_addr, register, value)
186
218
 
187
- def write_multi_command(self, comm_addr: int, offset: int, values: bytes) -> ProtocolCommand:
219
+ def write_multi_command(self, offset: int, values: bytes) -> ProtocolCommand:
188
220
  """Create write multiple protocol command."""
189
- return ModbusTcpWriteMultiCommand(comm_addr, offset, values)
221
+ return ModbusTcpWriteMultiCommand(self._comm_addr, offset, values)
190
222
 
191
223
  async def _connect(self) -> None:
192
224
  if not self._transport or self._transport.is_closing():
@@ -195,6 +227,14 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
195
227
  lambda: self,
196
228
  host=self._host, port=self._port,
197
229
  )
230
+ sock = self._transport.get_extra_info('socket')
231
+ if sock is not None:
232
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
233
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 10)
234
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
235
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
236
+ if platform.system() == 'Windows':
237
+ sock.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 10000, 10000))
198
238
 
199
239
  def connection_made(self, transport: asyncio.DatagramTransport) -> None:
200
240
  """On connection made"""
@@ -203,7 +243,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
203
243
 
204
244
  def eof_received(self) -> None:
205
245
  logger.debug("EOF received.")
206
- self._close_transport()
246
+ self.close_transport()
207
247
 
208
248
  def connection_lost(self, exc: Optional[Exception]) -> None:
209
249
  """On connection lost"""
@@ -211,37 +251,50 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
211
251
  logger.debug("Connection closed with error: %s.", exc)
212
252
  else:
213
253
  logger.debug("Connection closed.")
214
- self._close_transport()
254
+ self.close_transport()
215
255
 
216
256
  def data_received(self, data: bytes) -> None:
217
257
  """On data received"""
218
258
  if self._timer:
219
259
  self._timer.cancel()
220
260
  try:
261
+ if self._partial_data:
262
+ logger.debug("Received another response fragment: %s.", data.hex())
263
+ data = self._partial_data + data
221
264
  if self.command.validator(data):
222
- logger.debug("Received: %s", data.hex())
265
+ if self._partial_data:
266
+ logger.debug("Composed fragmented response: %s", data.hex())
267
+ else:
268
+ logger.debug("Received: %s", data.hex())
223
269
  self._retry = 0
270
+ self._partial_data = None
224
271
  self.response_future.set_result(data)
225
272
  else:
226
273
  logger.debug("Received invalid response: %s", data.hex())
227
274
  self.response_future.set_exception(RequestRejectedException())
228
- self._close_transport()
275
+ self.close_transport()
276
+ except PartialResponseException:
277
+ logger.debug("Received response fragment: %s", data.hex())
278
+ self._partial_data = data
279
+ return
280
+ except asyncio.InvalidStateError:
281
+ logger.debug("Response already handled: %s", data.hex())
229
282
  except RequestRejectedException as ex:
230
283
  logger.debug("Received exception response: %s", data.hex())
231
284
  self.response_future.set_exception(ex)
232
- # self._close_transport()
285
+ # self.close_transport()
233
286
 
234
287
  def error_received(self, exc: Exception) -> None:
235
288
  """On error received"""
236
289
  logger.debug("Received error: %s", exc)
237
290
  self.response_future.set_exception(exc)
238
- self._close_transport()
291
+ self.close_transport()
239
292
 
240
293
  async def send_request(self, command: ProtocolCommand) -> Future:
241
294
  """Send message via transport"""
242
295
  await self._ensure_lock().acquire()
243
296
  try:
244
- await self._connect()
297
+ await asyncio.wait_for(self._connect(), timeout=5)
245
298
  response_future = asyncio.get_running_loop().create_future()
246
299
  self._send_request(command, response_future)
247
300
  await response_future
@@ -249,17 +302,17 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
249
302
  except asyncio.CancelledError:
250
303
  if self._retry < self.retries:
251
304
  if self._timer:
252
- logger.debug("Connection broken error")
305
+ logger.debug("Connection broken error.")
253
306
  self._retry += 1
254
307
  if self._lock and self._lock.locked():
255
308
  self._lock.release()
256
- self._close_transport()
309
+ self.close_transport()
257
310
  return await self.send_request(command)
258
311
  else:
259
312
  return self._max_retries_reached()
260
- except (ConnectionRefusedError, TimeoutError) as exc:
313
+ except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError):
261
314
  if self._retry < self.retries:
262
- logger.debug("Connection refused error: %s", exc)
315
+ logger.debug("Connection refused error.")
263
316
  self._retry += 1
264
317
  if self._lock and self._lock.locked():
265
318
  self._lock.release()
@@ -274,9 +327,12 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
274
327
  """Send message via transport"""
275
328
  self.command = command
276
329
  self.response_future = response_future
277
- logger.debug("Sending: %s%s", self.command,
278
- f' - retry #{self._retry}/{self.retries}' if self._retry > 0 else '')
279
- self._transport.write(self.command.request)
330
+ payload = command.request_bytes()
331
+ if self._retry > 0:
332
+ logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
333
+ else:
334
+ logger.debug("Sending: %s", self.command)
335
+ self._transport.write(payload)
280
336
  self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
281
337
 
282
338
  def _timeout_mechanism(self) -> None:
@@ -287,16 +343,16 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
287
343
  if self._timer:
288
344
  logger.debug("Failed to receive response to %s in time (%ds).", self.command, self.timeout)
289
345
  self._timer = None
290
- self._close_transport()
346
+ self.close_transport()
291
347
 
292
348
  def _max_retries_reached(self) -> Future:
293
349
  logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
294
- self._close_transport()
350
+ self.close_transport()
295
351
  self.response_future = asyncio.get_running_loop().create_future()
296
352
  self.response_future.set_exception(MaxRetriesException)
297
353
  return self.response_future
298
354
 
299
- def _close_transport(self) -> None:
355
+ def close_transport(self) -> None:
300
356
  if self._transport:
301
357
  try:
302
358
  self._transport.close()
@@ -354,6 +410,10 @@ class ProtocolCommand:
354
410
  def __repr__(self):
355
411
  return self.request.hex()
356
412
 
413
+ def request_bytes(self) -> bytes:
414
+ """Return raw bytes payload, optionally pre-processed"""
415
+ return self.request
416
+
357
417
  def trim_response(self, raw_response: bytes):
358
418
  """Trim raw response from header and checksum data"""
359
419
  return raw_response
@@ -567,6 +627,12 @@ class ModbusTcpProtocolCommand(ProtocolCommand):
567
627
  self.first_address: int = offset
568
628
  self.value = value
569
629
 
630
+ def request_bytes(self) -> bytes:
631
+ """Return raw bytes payload, optionally pre-processed"""
632
+ # Apply sequential Modbus/TCP transaction identifier
633
+ self.request = _next_tx() + self.request[2:]
634
+ return self.request
635
+
570
636
  def trim_response(self, raw_response: bytes):
571
637
  """Trim raw response from header and checksum data"""
572
638
  return raw_response[9:]
goodwe/sensor.py CHANGED
@@ -183,7 +183,7 @@ class Energy(Sensor):
183
183
 
184
184
  def read_value(self, data: ProtocolResponse):
185
185
  value = read_bytes2(data)
186
- return float(value) / 10 if value else None
186
+ return float(value) / 10 if value is not None else None
187
187
 
188
188
 
189
189
  class Energy4(Sensor):
@@ -194,7 +194,7 @@ class Energy4(Sensor):
194
194
 
195
195
  def read_value(self, data: ProtocolResponse):
196
196
  value = read_bytes4(data)
197
- return float(value) / 10 if value else None
197
+ return float(value) / 10 if value is not None else None
198
198
 
199
199
 
200
200
  class Apparent(Sensor):
@@ -515,6 +515,14 @@ class EcoMode(ABC):
515
515
  def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
516
516
  """Set the schedule type"""
517
517
 
518
+ @abstractmethod
519
+ def get_power(self) -> int:
520
+ """Answer the power value"""
521
+
522
+ @abstractmethod
523
+ def get_power_unit(self) -> str:
524
+ """Answer the power unit"""
525
+
518
526
 
519
527
  class EcoModeV1(Sensor, EcoMode):
520
528
  """Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
@@ -606,6 +614,14 @@ class EcoModeV1(Sensor, EcoMode):
606
614
  """Set the schedule type"""
607
615
  pass
608
616
 
617
+ def get_power(self) -> int:
618
+ """Answer the power value"""
619
+ return self.power
620
+
621
+ def get_power_unit(self) -> str:
622
+ """Answer the power unit"""
623
+ return "%"
624
+
609
625
  def as_eco_mode_v2(self) -> EcoModeV2:
610
626
  """Convert V1 to V2 EcoMode"""
611
627
  result = EcoModeV2(self.id_, self.offset, self.name)
@@ -642,7 +658,7 @@ class Schedule(Sensor, EcoMode):
642
658
  def __str__(self):
643
659
  return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} " \
644
660
  f"{self.months + ' ' if self.months else ''}" \
645
- f"{self.schedule_type.decode_power(self.power)}{self.schedule_type.power_unit()} (SoC {self.soc}%) " \
661
+ f"{self.get_power()}{self.get_power_unit()} (SoC {self.soc}%) " \
646
662
  f"{'On' if -10 < self.on_off < 0 else 'Off' if 10 > self.on_off >= 0 else 'Unset'}"
647
663
 
648
664
  def read_value(self, data: ProtocolResponse):
@@ -736,6 +752,14 @@ class Schedule(Sensor, EcoMode):
736
752
  else:
737
753
  self.schedule_type = schedule_type
738
754
 
755
+ def get_power(self) -> int:
756
+ """Answer the power value"""
757
+ return self.schedule_type.decode_power(self.power)
758
+
759
+ def get_power_unit(self) -> str:
760
+ """Answer the power unit"""
761
+ return self.schedule_type.power_unit()
762
+
739
763
  def as_eco_mode_v1(self) -> EcoModeV1:
740
764
  """Convert V2 to V1 EcoMode"""
741
765
  result = EcoModeV1(self.id_, self.offset, self.name)
@@ -886,7 +910,7 @@ def read_temp(buffer: ProtocolResponse, offset: int = None) -> float | None:
886
910
  if offset is not None:
887
911
  buffer.seek(offset)
888
912
  value = int.from_bytes(buffer.read(2), byteorder="big", signed=True)
889
- if value == 32767:
913
+ if value == -1 or value == 32767:
890
914
  return None
891
915
  else:
892
916
  return float(value) / 10
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.0
3
+ Version: 0.4.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=0Zwuri1cbJ2Qe24R2rEjDMTZeVtsh21YIx3KlRaXgWg,5742
2
+ goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
+ goodwe/dt.py,sha256=oGbkdVHP51KnlwQraKeebmiP6AtJ1S67aLB7euNRIoE,11743
4
+ goodwe/es.py,sha256=iVK8EMCaAJJFihZLntJZ_Eu4sQWoZTVtTROp9mHFG6o,22730
5
+ goodwe/et.py,sha256=CiX-PE7wouDnj1RnPnOyqiNE4FELhOGdyPUOm9VCzUw,43890
6
+ goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
7
+ goodwe/inverter.py,sha256=-eRq6ND-BpLmj6vgYW0K0Oq3WvNcjjScbkalAzPH5ew,10494
8
+ goodwe/modbus.py,sha256=zT3W9ByANPaZd7T0XTqYGBaVo9PEwyg8jus12mRxCPU,8211
9
+ goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
+ goodwe/protocol.py,sha256=m4n1VAonXLBswFEjUcvKXEPV2WcOv_-MDMAefpsQ_-g,27703
11
+ goodwe/sensor.py,sha256=buPG8BcgZmRDqaMrLQUACLHB85U134qG6qo_ggsu48A,37679
12
+ goodwe-0.4.2.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.4.2.dist-info/METADATA,sha256=gOkkNodwpHtUf_743Nc7jCKpdxjwX_L5DSr2POJDjs8,3376
14
+ goodwe-0.4.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.4.2.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.4.2.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- goodwe/__init__.py,sha256=0Zwuri1cbJ2Qe24R2rEjDMTZeVtsh21YIx3KlRaXgWg,5742
2
- goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
- goodwe/dt.py,sha256=q8PRs0nVqN4mEhH8243NTbkkBtrGx-n8icwE-BkTN5Q,10460
4
- goodwe/es.py,sha256=gnSla5SGXK3cJag45o9Z2Wd7rwLkjm3xmS-JN1lf5Ck,22545
5
- goodwe/et.py,sha256=VJxCl54DILBRFQTmm-9K2yqS_QbBVMDvPFNv8dr0z7Y,39676
6
- goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
- goodwe/inverter.py,sha256=JIKYcOLihxCG1_m7HGMoFgVR1dyO8F0OXP5q1ClQJ-w,10336
8
- goodwe/modbus.py,sha256=sFmkBgylwJkZd64a52fOUst6Rde5Vm3JsAm3Nh3s6e8,8151
9
- goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
- goodwe/protocol.py,sha256=_jPwIlKE5ou2X3_3PDTUzsgBLLD1dzAdyt5DJOsWTWA,24873
11
- goodwe/sensor.py,sha256=fWMYyr3Vw02axfGvL7y7YUH2LmfE4A_lsIulX0Zpy5c,37054
12
- goodwe-0.4.0.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.4.0.dist-info/METADATA,sha256=CRS4h7iSlxExGN2ZjLungFGwuI7d85g-TlVRbnPx6vU,3376
14
- goodwe-0.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.4.0.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.4.0.dist-info/RECORD,,
File without changes