goodwe 0.4.7__py3-none-any.whl → 0.4.9__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/protocol.py CHANGED
@@ -1,3 +1,4 @@
1
+ """Low level IP communication protocol implementation."""
1
2
  from __future__ import annotations
2
3
 
3
4
  import asyncio
@@ -6,7 +7,7 @@ import logging
6
7
  import platform
7
8
  import socket
8
9
  from asyncio.futures import Future
9
- from typing import Tuple, Optional, Callable
10
+ from typing import Optional, Callable
10
11
 
11
12
  from .exceptions import MaxRetriesException, PartialResponseException, RequestFailedException, RequestRejectedException
12
13
  from .modbus import create_modbus_rtu_request, create_modbus_rtu_multi_request, create_modbus_tcp_request, \
@@ -37,7 +38,7 @@ class InverterProtocol:
37
38
  self._timer: asyncio.TimerHandle | None = None
38
39
  self.timeout: int = timeout
39
40
  self.retries: int = retries
40
- self.keep_alive: bool = True
41
+ self.keep_alive: bool = False
41
42
  self.protocol: asyncio.Protocol | None = None
42
43
  self.response_future: Future | None = None
43
44
  self.command: ProtocolCommand | None = None
@@ -55,12 +56,29 @@ class InverterProtocol:
55
56
  """
56
57
  if self._lock and self._running_loop == asyncio.get_event_loop():
57
58
  return self._lock
58
- else:
59
- logger.debug("Creating lock instance for current event loop.")
60
- self._lock = asyncio.Lock()
61
- self._running_loop = asyncio.get_event_loop()
62
- self._close_transport()
63
- return self._lock
59
+ logger.debug("Creating lock instance for current event loop.")
60
+ self._lock = asyncio.Lock()
61
+ self._running_loop = asyncio.get_event_loop()
62
+ self._close_transport()
63
+ return self._lock
64
+
65
+ def _max_retries_reached(self) -> Future:
66
+ logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
67
+ self._close_transport()
68
+ self.response_future = asyncio.get_running_loop().create_future()
69
+ self.response_future.set_exception(MaxRetriesException)
70
+ return self.response_future
71
+
72
+ def _close_transport(self) -> None:
73
+ if self._transport:
74
+ try:
75
+ self._transport.close()
76
+ except RuntimeError:
77
+ logger.debug("Failed to close transport.")
78
+ self._transport = None
79
+ # Cancel Future on connection lost
80
+ if self.response_future and not self.response_future.done():
81
+ self.response_future.cancel()
64
82
 
65
83
  async def close(self) -> None:
66
84
  """Close the underlying transport/connection."""
@@ -103,9 +121,11 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
103
121
 
104
122
  async def _connect(self) -> None:
105
123
  if not self._transport or self._transport.is_closing():
124
+ allow_broadcast = platform.system() == "Darwin" and self._host == "255.255.255.255"
106
125
  self._transport, self.protocol = await asyncio.get_running_loop().create_datagram_endpoint(
107
126
  lambda: self,
108
127
  remote_addr=(self._host, self._port),
128
+ allow_broadcast=allow_broadcast,
109
129
  )
110
130
 
111
131
  def connection_made(self, transport: asyncio.DatagramTransport) -> None:
@@ -120,7 +140,7 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
120
140
  logger.debug("Socket closed.")
121
141
  self._close_transport()
122
142
 
123
- def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
143
+ def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None:
124
144
  """On datagram received"""
125
145
  if self._timer:
126
146
  self._timer.cancel()
@@ -133,15 +153,16 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
133
153
  self._partial_missing = 0
134
154
  if self.command.validator(data):
135
155
  logger.debug("Received: %s", data.hex())
156
+ self._retry = 0
136
157
  self.response_future.set_result(data)
137
158
  else:
138
159
  logger.debug("Received invalid response: %s", data.hex())
139
- asyncio.get_running_loop().call_soon(self._retry_mechanism)
160
+ asyncio.get_running_loop().call_soon(self._timeout_mechanism)
140
161
  except PartialResponseException as ex:
141
162
  logger.debug("Received response fragment (%d of %d): %s", ex.length, ex.expected, data.hex())
142
163
  self._partial_data = data
143
164
  self._partial_missing = ex.expected - ex.length
144
- self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
165
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
145
166
  except asyncio.InvalidStateError:
146
167
  logger.debug("Response already handled: %s", data.hex())
147
168
  except RequestRejectedException as ex:
@@ -158,13 +179,27 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
158
179
 
159
180
  async def send_request(self, command: ProtocolCommand) -> Future:
160
181
  """Send message via transport"""
161
- async with self._ensure_lock():
182
+ await self._ensure_lock().acquire()
183
+ try:
162
184
  await self._connect()
163
185
  response_future = asyncio.get_running_loop().create_future()
164
- self._retry = 0
165
186
  self._send_request(command, response_future)
166
187
  await response_future
167
188
  return response_future
189
+ except asyncio.CancelledError:
190
+ if self._retry < self.retries:
191
+ self._retry += 1
192
+ if self._lock and self._lock.locked():
193
+ self._lock.release()
194
+ if not self.keep_alive:
195
+ self._close_transport()
196
+ return await self.send_request(command)
197
+ return self._max_retries_reached()
198
+ finally:
199
+ if self._lock and self._lock.locked():
200
+ self._lock.release()
201
+ if not self.keep_alive:
202
+ self._close_transport()
168
203
 
169
204
  def _send_request(self, command: ProtocolCommand, response_future: Future) -> None:
170
205
  """Send message via transport"""
@@ -178,32 +213,19 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
178
213
  else:
179
214
  logger.debug("Sending: %s", self.command)
180
215
  self._transport.sendto(payload)
181
- self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
216
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
182
217
 
183
- def _retry_mechanism(self) -> None:
184
- """Retry mechanism to prevent hanging transport"""
185
- if self.response_future.done():
218
+ def _timeout_mechanism(self) -> None:
219
+ """Timeout mechanism to prevent hanging transport"""
220
+ if self.response_future and self.response_future.done():
186
221
  logger.debug("Response already received.")
187
- elif self._retry < self.retries:
222
+ self._retry = 0
223
+ else:
188
224
  if self._timer:
189
225
  logger.debug("Failed to receive response to %s in time (%ds).", self.command, self.timeout)
190
- self._retry += 1
191
- self._send_request(self.command, self.response_future)
192
- else:
193
- logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
194
- self.response_future.set_exception(MaxRetriesException)
195
- self._close_transport()
196
-
197
- def _close_transport(self) -> None:
198
- if self._transport:
199
- try:
200
- self._transport.close()
201
- except RuntimeError:
202
- logger.debug("Failed to close transport.")
203
- self._transport = None
204
- # Cancel Future on connection close
205
- if self.response_future and not self.response_future.done():
206
- self.response_future.cancel()
226
+ self._timer = None
227
+ if self.response_future and not self.response_future.done():
228
+ self.response_future.cancel()
207
229
 
208
230
  async def close(self):
209
231
  self._close_transport()
@@ -250,7 +272,6 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
250
272
  def connection_made(self, transport: asyncio.DatagramTransport) -> None:
251
273
  """On connection made"""
252
274
  logger.debug("Connection opened.")
253
- pass
254
275
 
255
276
  def eof_received(self) -> None:
256
277
  logger.debug("EOF received.")
@@ -319,8 +340,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
319
340
  self._lock.release()
320
341
  self._close_transport()
321
342
  return await self.send_request(command)
322
- else:
323
- return self._max_retries_reached()
343
+ return self._max_retries_reached()
324
344
  except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError):
325
345
  if self._retry < self.retries:
326
346
  logger.debug("Connection refused error.")
@@ -328,8 +348,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
328
348
  if self._lock and self._lock.locked():
329
349
  self._lock.release()
330
350
  return await self.send_request(command)
331
- else:
332
- return self._max_retries_reached()
351
+ return self._max_retries_reached()
333
352
  finally:
334
353
  if self._lock and self._lock.locked():
335
354
  self._lock.release()
@@ -358,24 +377,6 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
358
377
  self._timer = None
359
378
  self._close_transport()
360
379
 
361
- def _max_retries_reached(self) -> Future:
362
- logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
363
- self._close_transport()
364
- self.response_future = asyncio.get_running_loop().create_future()
365
- self.response_future.set_exception(MaxRetriesException)
366
- return self.response_future
367
-
368
- def _close_transport(self) -> None:
369
- if self._transport:
370
- try:
371
- self._transport.close()
372
- except RuntimeError:
373
- logger.debug("Failed to close transport.")
374
- self._transport = None
375
- # Cancel Future on connection lost
376
- if self.response_future and not self.response_future.done():
377
- self.response_future.cancel()
378
-
379
380
  async def close(self):
380
381
  await self._ensure_lock().acquire()
381
382
  try:
@@ -399,8 +400,7 @@ class ProtocolResponse:
399
400
  def response_data(self) -> bytes:
400
401
  if self.command is not None:
401
402
  return self.command.trim_response(self.raw_data)
402
- else:
403
- return self.raw_data
403
+ return self.raw_data
404
404
 
405
405
  def seek(self, address: int) -> None:
406
406
  if self.command is not None:
@@ -454,10 +454,9 @@ class ProtocolCommand:
454
454
  result = response_future.result()
455
455
  if result is not None:
456
456
  return ProtocolResponse(result, self)
457
- else:
458
- raise RequestFailedException(
459
- "No response received to '" + self.request.hex() + "' request."
460
- )
457
+ raise RequestFailedException(
458
+ "No response received to '" + self.request.hex() + "' request."
459
+ )
461
460
  except (asyncio.CancelledError, ConnectionRefusedError):
462
461
  raise RequestFailedException(
463
462
  "No valid response received to '" + self.request.hex() + "' request."
@@ -540,12 +539,11 @@ class Aa55ProtocolCommand(ProtocolCommand):
540
539
  if self.request[4] == 1:
541
540
  if self.request[5] == 2:
542
541
  return f'READ device info ({self.request.hex()})'
543
- elif self.request[5] == 6:
542
+ if self.request[5] == 6:
544
543
  return f'READ runtime data ({self.request.hex()})'
545
- elif self.request[5] == 9:
544
+ if self.request[5] == 9:
546
545
  return f'READ settings ({self.request.hex()})'
547
- else:
548
- return self.request.hex()
546
+ return self.request.hex()
549
547
 
550
548
 
551
549
  class Aa55ReadCommand(Aa55ProtocolCommand):
@@ -554,13 +552,12 @@ class Aa55ReadCommand(Aa55ProtocolCommand):
554
552
  """
555
553
 
556
554
  def __init__(self, offset: int, count: int):
557
- super().__init__("011A03" + "{:04x}".format(offset) + "{:02x}".format(count), "019A", offset, count)
555
+ super().__init__(f"011A03{offset:04x}{count:02x}", "019A", offset, count)
558
556
 
559
557
  def __repr__(self):
560
558
  if self.value > 1:
561
559
  return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
562
- else:
563
- return f'READ register {self.first_address} ({self.request.hex()})'
560
+ return f'READ register {self.first_address} ({self.request.hex()})'
564
561
 
565
562
 
566
563
  class Aa55WriteCommand(Aa55ProtocolCommand):
@@ -569,7 +566,7 @@ class Aa55WriteCommand(Aa55ProtocolCommand):
569
566
  """
570
567
 
571
568
  def __init__(self, register: int, value: int):
572
- super().__init__("023905" + "{:04x}".format(register) + "01" + "{:04x}".format(value), "02B9", register, value)
569
+ super().__init__(f"023905{register:04x}01{value:04x}", "02B9", register, value)
573
570
 
574
571
  def __repr__(self):
575
572
  return f'WRITE {self.value} to register {self.first_address} ({self.request.hex()})'
@@ -581,7 +578,7 @@ class Aa55WriteMultiCommand(Aa55ProtocolCommand):
581
578
  """
582
579
 
583
580
  def __init__(self, offset: int, values: bytes):
584
- super().__init__("02390B" + "{:04x}".format(offset) + "{:02x}".format(len(values)) + values.hex(),
581
+ super().__init__(f"02390B{offset:04x}{len(values):02x}{values.hex()}",
585
582
  "02B9", offset, len(values) // 2)
586
583
 
587
584
 
@@ -637,8 +634,7 @@ class ModbusRtuReadCommand(ModbusRtuProtocolCommand):
637
634
  def __repr__(self):
638
635
  if self.value > 1:
639
636
  return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
640
- else:
641
- return f'READ register {self.first_address} ({self.request.hex()})'
637
+ return f'READ register {self.first_address} ({self.request.hex()})'
642
638
 
643
639
 
644
640
  class ModbusRtuWriteCommand(ModbusRtuProtocolCommand):
@@ -707,8 +703,7 @@ class ModbusTcpReadCommand(ModbusTcpProtocolCommand):
707
703
  def __repr__(self):
708
704
  if self.value > 1:
709
705
  return f'READ {self.value} registers from {self.first_address} ({self.request.hex()})'
710
- else:
711
- return f'READ register {self.first_address} ({self.request.hex()})'
706
+ return f'READ register {self.first_address} ({self.request.hex()})'
712
707
 
713
708
 
714
709
  class ModbusTcpWriteCommand(ModbusTcpProtocolCommand):