goodwe 0.4.3__tar.gz → 0.4.5__tar.gz
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-0.4.3/goodwe.egg-info → goodwe-0.4.5}/PKG-INFO +1 -1
- goodwe-0.4.5/VERSION +1 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/modbus.py +7 -6
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/protocol.py +38 -26
- {goodwe-0.4.3 → goodwe-0.4.5/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.4.3 → goodwe-0.4.5}/tests/test_dt.py +63 -0
- goodwe-0.4.3/VERSION +0 -1
- {goodwe-0.4.3 → goodwe-0.4.5}/LICENSE +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/README.md +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/__init__.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/const.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/dt.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/es.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/et.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/exceptions.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/inverter.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/model.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe/sensor.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/pyproject.toml +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/setup.cfg +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/tests/test_es.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/tests/test_et.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/tests/test_modbus.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/tests/test_protocol.py +0 -0
- {goodwe-0.4.3 → goodwe-0.4.5}/tests/test_sensor.py +0 -0
goodwe-0.4.5/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.5
|
|
@@ -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
|
-
|
|
225
|
-
# The
|
|
226
|
-
|
|
227
|
-
|
|
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),
|
|
239
|
-
|
|
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)
|
|
@@ -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("
|
|
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
|
-
|
|
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.
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
@@ -416,3 +416,66 @@ class GW20KAU_DT_Test(DtMock):
|
|
|
416
416
|
self.assertSensor('vnbus', 298.9, 'V', data)
|
|
417
417
|
self.assertSensor('derating_mode', 4, '', data)
|
|
418
418
|
self.assertSensor('derating_mode_label', 'Reactive power derating(PF/QU/FixQ)', '', data)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class GW17K_DT_Test(DtMock):
|
|
422
|
+
|
|
423
|
+
def __init__(self, methodName='runTest'):
|
|
424
|
+
DtMock.__init__(self, methodName)
|
|
425
|
+
self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW17K-DT_running_data.hex')
|
|
426
|
+
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW17K-DT_device_info.hex')
|
|
427
|
+
|
|
428
|
+
def test_GW20KAU_DT_device_info(self):
|
|
429
|
+
self.loop.run_until_complete(self.read_device_info())
|
|
430
|
+
self.assertEqual('GW17KT-DT', self.model_name)
|
|
431
|
+
self.assertEqual('5017KDTT00BW0000', self.serial_number)
|
|
432
|
+
self.assertEqual(12, self.dsp1_version)
|
|
433
|
+
self.assertEqual(12, self.dsp2_version)
|
|
434
|
+
self.assertEqual(13, self.arm_version)
|
|
435
|
+
self.assertEqual('12.12.0d', self.firmware)
|
|
436
|
+
|
|
437
|
+
def test_GW20KAU_DT_runtime_data(self):
|
|
438
|
+
self.loop.run_until_complete(self.read_device_info())
|
|
439
|
+
data = self.loop.run_until_complete(self.read_runtime_data())
|
|
440
|
+
self.assertEqual(40, len(data))
|
|
441
|
+
|
|
442
|
+
self.assertSensor('timestamp', datetime.strptime('2024-05-20 10:35:55', '%Y-%m-%d %H:%M:%S'), '', data)
|
|
443
|
+
self.assertSensor('vpv1', 540.0, 'V', data)
|
|
444
|
+
self.assertSensor('ipv1', 10.5, 'A', data)
|
|
445
|
+
self.assertSensor('ppv1', 5670, 'W', data)
|
|
446
|
+
self.assertSensor('vpv2', 475.5, 'V', data)
|
|
447
|
+
self.assertSensor('ipv2', 14.8, 'A', data)
|
|
448
|
+
self.assertSensor('ppv2', 7037, 'W', data)
|
|
449
|
+
self.assertSensor('vline1', 413.0, 'V', data)
|
|
450
|
+
self.assertSensor('vline2', 411.5, 'V', data)
|
|
451
|
+
self.assertSensor('vline3', 409.5, 'V', data)
|
|
452
|
+
self.assertSensor('vgrid1', 236.7, 'V', data)
|
|
453
|
+
self.assertSensor('vgrid2', 238.3, 'V', data)
|
|
454
|
+
self.assertSensor('vgrid3', 237.3, 'V', data)
|
|
455
|
+
self.assertSensor('igrid1', 17.6, 'A', data)
|
|
456
|
+
self.assertSensor('igrid2', 17.5, 'A', data)
|
|
457
|
+
self.assertSensor('igrid3', 17.5, 'A', data)
|
|
458
|
+
self.assertSensor('fgrid1', 50.02, 'Hz', data)
|
|
459
|
+
self.assertSensor('fgrid2', 50.02, 'Hz', data)
|
|
460
|
+
self.assertSensor('fgrid3', 50.02, 'Hz', data)
|
|
461
|
+
self.assertSensor('pgrid1', 4166, 'W', data)
|
|
462
|
+
self.assertSensor('pgrid2', 4170, 'W', data)
|
|
463
|
+
self.assertSensor('pgrid3', 4153, 'W', data)
|
|
464
|
+
self.assertSensor('ppv', 12470, 'W', data)
|
|
465
|
+
self.assertSensor('work_mode', 1, '', data)
|
|
466
|
+
self.assertSensor('work_mode_label', 'Normal', '', data)
|
|
467
|
+
self.assertSensor('error_codes', 0, '', data)
|
|
468
|
+
self.assertSensor('warning_code', 0, '', data)
|
|
469
|
+
self.assertSensor('apparent_power', 0, 'VA', data)
|
|
470
|
+
self.assertSensor('reactive_power', 0, 'var', data)
|
|
471
|
+
self.assertSensor('temperature', 45.7, 'C', data)
|
|
472
|
+
self.assertSensor('e_day', 29.3, 'kWh', data)
|
|
473
|
+
self.assertSensor('e_total', 29984.4, 'kWh', data)
|
|
474
|
+
self.assertSensor('h_total', 8357, 'h', data)
|
|
475
|
+
self.assertSensor('safety_country', 1, '', data)
|
|
476
|
+
self.assertSensor('safety_country_label', 'CZ-A1', '', data)
|
|
477
|
+
self.assertSensor('funbit', 546, '', data)
|
|
478
|
+
self.assertSensor('vbus', 621.8, 'V', data)
|
|
479
|
+
self.assertSensor('vnbus', 314.2, 'V', data)
|
|
480
|
+
self.assertSensor('derating_mode', 4, '', data)
|
|
481
|
+
self.assertSensor('derating_mode_label', 'Reactive power derating(PF/QU/FixQ)', '', data)
|
goodwe-0.4.3/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.4.3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|