goodwe 0.4.3__py3-none-any.whl → 0.4.5__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/modbus.py CHANGED
@@ -221,10 +221,11 @@ def validate_modbus_tcp_response(data: bytes, cmd: int, offset: int, value: int)
221
221
  if len(data) <= 8:
222
222
  logger.debug("Response is too short.")
223
223
  return False
224
- expected_length = int.from_bytes(data[4:6], byteorder='big', signed=False) + 6
225
- # The weird expected_length != 12 is work around Goodwe bug answering wrong (hardcoded 6) length.
226
- if len(data) < expected_length and expected_length != 12:
227
- raise PartialResponseException(len(data), expected_length)
224
+
225
+ # The Modbus/TCP message length check is completely ignore due to Goodwe bugs
226
+ # expected_length = int.from_bytes(data[4:6], byteorder='big', signed=False) + 6
227
+ # if len(data) < expected_length:
228
+ # raise PartialResponseException(len(data), expected_length)
228
229
 
229
230
  if data[7] == MODBUS_READ_CMD:
230
231
  expected_length = data[8] + 9
@@ -235,8 +236,8 @@ def validate_modbus_tcp_response(data: bytes, cmd: int, offset: int, value: int)
235
236
  return False
236
237
  elif data[7] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD):
237
238
  if len(data) < 12:
238
- logger.debug("Response has unexpected length: %d, expected %d.", len(data), 14)
239
- raise PartialResponseException(len(data), expected_length)
239
+ logger.debug("Response has unexpected length: %d, expected %d.", len(data), 12)
240
+ return False
240
241
  response_offset = int.from_bytes(data[8:10], byteorder='big', signed=False)
241
242
  if response_offset != offset:
242
243
  logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset)
goodwe/protocol.py CHANGED
@@ -42,6 +42,7 @@ class InverterProtocol:
42
42
  self.response_future: Future | None = None
43
43
  self.command: ProtocolCommand | None = None
44
44
  self._partial_data: bytes | None = None
45
+ self._partial_missing: int = 0
45
46
 
46
47
  def _ensure_lock(self) -> asyncio.Lock:
47
48
  """Validate (or create) asyncio Lock.
@@ -125,28 +126,28 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
125
126
  self._timer.cancel()
126
127
  self._timer = None
127
128
  try:
128
- if self._partial_data:
129
- logger.debug("Received another response fragment: %s.", data.hex())
129
+ if self._partial_data and self._partial_missing == len(data):
130
+ logger.debug("Composed fragmented response: %s + %s", self._partial_data.hex(), data.hex())
130
131
  data = self._partial_data + data
131
- if self.command.validator(data):
132
- if self._partial_data:
133
- logger.debug("Composed fragmented response: %s", data.hex())
134
- else:
135
- logger.debug("Received: %s", data.hex())
136
132
  self._partial_data = None
133
+ self._partial_missing = 0
134
+ if self.command.validator(data):
135
+ logger.debug("Received: %s", data.hex())
137
136
  self.response_future.set_result(data)
138
137
  else:
139
138
  logger.debug("Received invalid response: %s", data.hex())
140
139
  asyncio.get_running_loop().call_soon(self._retry_mechanism)
141
- except PartialResponseException:
142
- logger.debug("Received response fragment: %s", data.hex())
140
+ except PartialResponseException as ex:
141
+ logger.debug("Received response fragment (%d of %d): %s", ex.length, ex.expected, data.hex())
143
142
  self._partial_data = data
144
- return
143
+ self._partial_missing = ex.expected - ex.length
144
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
145
145
  except asyncio.InvalidStateError:
146
146
  logger.debug("Response already handled: %s", data.hex())
147
147
  except RequestRejectedException as ex:
148
148
  logger.debug("Received exception response: %s", data.hex())
149
- self.response_future.set_exception(ex)
149
+ if self.response_future and not self.response_future.done():
150
+ self.response_future.set_exception(ex)
150
151
  self._close_transport()
151
152
 
152
153
  def error_received(self, exc: Exception) -> None:
@@ -169,6 +170,8 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
169
170
  """Send message via transport"""
170
171
  self.command = command
171
172
  self.response_future = response_future
173
+ self._partial_data = None
174
+ self._partial_missing = 0
172
175
  payload = command.request_bytes()
173
176
  if self._retry > 0:
174
177
  logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
@@ -266,30 +269,30 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
266
269
  if self._timer:
267
270
  self._timer.cancel()
268
271
  try:
269
- if self._partial_data:
270
- logger.debug("Received another response fragment: %s.", data.hex())
272
+ if self._partial_data and self._partial_missing == len(data):
273
+ logger.debug("Composed fragmented response: %s + %s", self._partial_data.hex(), data.hex())
271
274
  data = self._partial_data + data
275
+ self._partial_data = None
276
+ self._partial_missing = 0
272
277
  if self.command.validator(data):
273
- if self._partial_data:
274
- logger.debug("Composed fragmented response: %s", data.hex())
275
- else:
276
- logger.debug("Received: %s", data.hex())
278
+ logger.debug("Received: %s", data.hex())
277
279
  self._retry = 0
278
- self._partial_data = None
279
280
  self.response_future.set_result(data)
280
281
  else:
281
282
  logger.debug("Received invalid response: %s", data.hex())
282
283
  self.response_future.set_exception(RequestRejectedException())
283
284
  self._close_transport()
284
- except PartialResponseException:
285
- logger.debug("Received response fragment: %s", data.hex())
285
+ except PartialResponseException as ex:
286
+ logger.debug("Received response fragment (%d of %d): %s", ex.length, ex.expected, data.hex())
286
287
  self._partial_data = data
287
- return
288
+ self._partial_missing = ex.expected - ex.length
289
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
288
290
  except asyncio.InvalidStateError:
289
291
  logger.debug("Response already handled: %s", data.hex())
290
292
  except RequestRejectedException as ex:
291
293
  logger.debug("Received exception response: %s", data.hex())
292
- self.response_future.set_exception(ex)
294
+ if self.response_future and not self.response_future.done():
295
+ self.response_future.set_exception(ex)
293
296
  # self._close_transport()
294
297
 
295
298
  def error_received(self, exc: Exception) -> None:
@@ -335,6 +338,8 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
335
338
  """Send message via transport"""
336
339
  self.command = command
337
340
  self.response_future = response_future
341
+ self._partial_data = None
342
+ self._partial_missing = 0
338
343
  payload = command.request_bytes()
339
344
  if self._retry > 0:
340
345
  logger.debug("Sending: %s - retry #%s/%s", self.command, self._retry, self.retries)
@@ -468,7 +473,7 @@ class Aa55ProtocolCommand(ProtocolCommand):
468
473
  Quite probably it is some variation of the protocol used on RS-485 serial link,
469
474
  extended/adapted to UDP transport layer.
470
475
 
471
- Each request starts with header of 0xAA, 0x55, then 0xC0, 0x7F (probably some sort of address/command)
476
+ Each request starts with header of 0xAA, 0x55, then 0xC0, 0x7F (client addr, inverter addr)
472
477
  followed by actual payload data.
473
478
  It is suffixed with 2 bytes of plain checksum of header+payload.
474
479
 
@@ -484,7 +489,7 @@ class Aa55ProtocolCommand(ProtocolCommand):
484
489
  + payload
485
490
  + self._checksum(bytes.fromhex("AA55C07F" + payload)).hex()
486
491
  ),
487
- lambda x: self._validate_response(x, response_type),
492
+ lambda x: self._validate_aa55_response(x, response_type),
488
493
  )
489
494
 
490
495
  @staticmethod
@@ -495,7 +500,7 @@ class Aa55ProtocolCommand(ProtocolCommand):
495
500
  return checksum.to_bytes(2, byteorder="big", signed=False)
496
501
 
497
502
  @staticmethod
498
- def _validate_response(data: bytes, response_type: str) -> bool:
503
+ def _validate_aa55_response(data: bytes, response_type: str) -> bool:
499
504
  """
500
505
  Validate the response.
501
506
  data[0:3] is header
@@ -503,13 +508,20 @@ class Aa55ProtocolCommand(ProtocolCommand):
503
508
  data[6] is response payload length
504
509
  data[-2:] is checksum (plain sum of response data incl. header)
505
510
  """
506
- if len(data) <= 8 or len(data) != data[6] + 9:
511
+ if len(data) <= 8:
512
+ logger.debug("Response too short.")
513
+ return False
514
+ elif len(data) < data[6] + 9:
507
515
  raise PartialResponseException(len(data), data[6] + 9)
516
+ elif len(data) > data[6] + 9:
517
+ logger.debug("Response invalid - too long (%d).", len(data))
518
+ return False
508
519
  elif response_type:
509
520
  data_rt_int = int.from_bytes(data[4:6], byteorder="big", signed=True)
510
521
  if int(response_type, 16) != data_rt_int:
511
522
  logger.debug("Response type unexpected: %04x, expected %s.", data_rt_int, response_type)
512
523
  return False
524
+
513
525
  checksum = 0
514
526
  for each in data[:-2]:
515
527
  checksum += each
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.3
3
+ Version: 0.4.5
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
@@ -5,12 +5,12 @@ goodwe/es.py,sha256=iVK8EMCaAJJFihZLntJZ_Eu4sQWoZTVtTROp9mHFG6o,22730
5
5
  goodwe/et.py,sha256=CiX-PE7wouDnj1RnPnOyqiNE4FELhOGdyPUOm9VCzUw,43890
6
6
  goodwe/exceptions.py,sha256=dKMLxotjoR1ic8OVlw1joIJ4mKWD6oFtUMZ86fNM5ZE,1403
7
7
  goodwe/inverter.py,sha256=86aMJzJjNOr1I_tCF5H6mBwzDTjLbGDKUL2hbi0XSxg,10459
8
- goodwe/modbus.py,sha256=qDFs8pMOtwgHPfwiZLd-P34vCLHc71-b8MQZMb8FJME,8488
8
+ goodwe/modbus.py,sha256=Mg_s_v8kbZgqXZM6ZUUxkZx2boAG8LkuDG5OiFKK2X4,8402
9
9
  goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
- goodwe/protocol.py,sha256=JhWYzUtCwbhxXCfZMA_hPGGCHcEEhn0y9B4goJ2GyNo,28306
10
+ goodwe/protocol.py,sha256=Ry1B-kki5F-AnsmpeUWuhP3eCCH1wrKKfDuT9BKfmvE,29140
11
11
  goodwe/sensor.py,sha256=buPG8BcgZmRDqaMrLQUACLHB85U134qG6qo_ggsu48A,37679
12
- goodwe-0.4.3.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.4.3.dist-info/METADATA,sha256=m3IqLUDf7Ae-ElSqvqJTNFmUu-ZaQWr1YWmjz7eqJaw,3376
14
- goodwe-0.4.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.4.3.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.4.3.dist-info/RECORD,,
12
+ goodwe-0.4.5.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.4.5.dist-info/METADATA,sha256=9MoleSeCTePnmqw6x_u5WcBE0GXwVvCtxF-9s4hqEHw,3376
14
+ goodwe-0.4.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.4.5.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.4.5.dist-info/RECORD,,
File without changes