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/__init__.py +49 -14
- goodwe/const.py +23 -17
- goodwe/dt.py +255 -111
- goodwe/es.py +235 -106
- goodwe/et.py +532 -201
- goodwe/exceptions.py +6 -3
- goodwe/inverter.py +209 -24
- goodwe/modbus.py +1 -0
- goodwe/model.py +106 -22
- goodwe/protocol.py +72 -77
- goodwe/sensor.py +83 -67
- {goodwe-0.4.7.dist-info → goodwe-0.4.9.dist-info}/METADATA +4 -4
- goodwe-0.4.9.dist-info/RECORD +16 -0
- {goodwe-0.4.7.dist-info → goodwe-0.4.9.dist-info}/WHEEL +1 -1
- goodwe-0.4.7.dist-info/RECORD +0 -16
- {goodwe-0.4.7.dist-info → goodwe-0.4.9.dist-info/licenses}/LICENSE +0 -0
- {goodwe-0.4.7.dist-info → goodwe-0.4.9.dist-info}/top_level.txt +0 -0
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
|
|
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 =
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
216
|
+
self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
|
|
182
217
|
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
self.
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
542
|
+
if self.request[5] == 6:
|
|
544
543
|
return f'READ runtime data ({self.request.hex()})'
|
|
545
|
-
|
|
544
|
+
if self.request[5] == 9:
|
|
546
545
|
return f'READ settings ({self.request.hex()})'
|
|
547
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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):
|