goodwe 0.4.7__tar.gz → 0.4.8__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.
Files changed (28) hide show
  1. {goodwe-0.4.7/goodwe.egg-info → goodwe-0.4.8}/PKG-INFO +1 -1
  2. goodwe-0.4.8/VERSION +1 -0
  3. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/dt.py +34 -10
  4. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/protocol.py +48 -45
  5. {goodwe-0.4.7 → goodwe-0.4.8/goodwe.egg-info}/PKG-INFO +1 -1
  6. {goodwe-0.4.7 → goodwe-0.4.8}/tests/test_dt.py +64 -33
  7. {goodwe-0.4.7 → goodwe-0.4.8}/tests/test_protocol.py +34 -34
  8. goodwe-0.4.7/VERSION +0 -1
  9. {goodwe-0.4.7 → goodwe-0.4.8}/LICENSE +0 -0
  10. {goodwe-0.4.7 → goodwe-0.4.8}/README.md +0 -0
  11. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/__init__.py +0 -0
  12. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/const.py +0 -0
  13. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/es.py +0 -0
  14. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/et.py +0 -0
  15. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/exceptions.py +0 -0
  16. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/inverter.py +0 -0
  17. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/modbus.py +0 -0
  18. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/model.py +0 -0
  19. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe/sensor.py +0 -0
  20. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe.egg-info/SOURCES.txt +0 -0
  21. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe.egg-info/dependency_links.txt +0 -0
  22. {goodwe-0.4.7 → goodwe-0.4.8}/goodwe.egg-info/top_level.txt +0 -0
  23. {goodwe-0.4.7 → goodwe-0.4.8}/pyproject.toml +0 -0
  24. {goodwe-0.4.7 → goodwe-0.4.8}/setup.cfg +0 -0
  25. {goodwe-0.4.7 → goodwe-0.4.8}/tests/test_es.py +0 -0
  26. {goodwe-0.4.7 → goodwe-0.4.8}/tests/test_et.py +0 -0
  27. {goodwe-0.4.7 → goodwe-0.4.8}/tests/test_modbus.py +0 -0
  28. {goodwe-0.4.7 → goodwe-0.4.8}/tests/test_sensor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.7
3
+ Version: 0.4.8
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
goodwe-0.4.8/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.8
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import Tuple
5
5
 
6
- from .exceptions import InverterError, RequestRejectedException
6
+ from .exceptions import InverterError, RequestFailedException, RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
@@ -69,7 +69,7 @@ class DT(Inverter):
69
69
  lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
70
70
  "On-grid L3 Power", "W", Kind.AC),
71
71
  # 30127 reserved
72
- PowerS("active_power", 30128, "Active Power", Kind.AC),
72
+ PowerS("total_inverter_power", 30128, "Total Power", Kind.AC),
73
73
  Integer("work_mode", 30129, "Work Mode code"),
74
74
  Enum2("work_mode_label", 30129, WORK_MODES, "Work Mode"),
75
75
  Long("error_codes", 30130, "Error Codes"),
@@ -78,7 +78,7 @@ class DT(Inverter):
78
78
  Reactive4("reactive_power", 30135, "Reactive Power", Kind.AC),
79
79
  # 30137 reserved
80
80
  # 30138 reserved
81
- # 30139 reserved
81
+ Decimal("power_factor", 30139, 1000, "Power Factor", "", Kind.GRID),
82
82
  # 30140 reserved
83
83
  Temp("temperature", 30141, "Inverter Temperature", Kind.AC),
84
84
  # 30142 reserved
@@ -113,6 +113,12 @@ class DT(Inverter):
113
113
  # 30172 reserved
114
114
  )
115
115
 
116
+ # Inverter's meter data
117
+ # Modbus registers from offset 0x75f4 (30196)
118
+ __all_sensors_meter: Tuple[Sensor, ...] = (
119
+ PowerS("active_power", 30196, "Active Power", Kind.GRID),
120
+ )
121
+
116
122
  # Modbus registers of inverter settings, offsets are modbus register addresses
117
123
  __all_settings: Tuple[Sensor, ...] = (
118
124
  Timestamp("time", 40313, "Inverter time"),
@@ -139,9 +145,12 @@ class DT(Inverter):
139
145
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
140
146
  super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
141
147
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
142
- self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
148
+ self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
149
+ self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f4, 0x01)
143
150
  self._sensors = self.__all_sensors
151
+ self._sensors_meter = self.__all_sensors_meter
144
152
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
153
+ self._has_meter: bool = True
145
154
 
146
155
  @staticmethod
147
156
  def _single_phase_only(s: Sensor) -> bool:
@@ -160,10 +169,13 @@ class DT(Inverter):
160
169
  self.model_name = response[22:32].decode("ascii").rstrip()
161
170
  except:
162
171
  print("No model name sent from the inverter.")
163
- self.serial_number = self._decode(response[6:22])
164
- self.dsp1_version = read_unsigned_int(response, 66)
165
- self.dsp2_version = read_unsigned_int(response, 68)
166
- self.arm_version = read_unsigned_int(response, 70)
172
+ # Modbus registers from 30001 - 30040
173
+ self.serial_number = self._decode(response[6:22]) # 30004 - 30012
174
+ self.dsp1_version = read_unsigned_int(response, 66) # 30034
175
+ self.dsp2_version = read_unsigned_int(response, 68) # 30035
176
+ self.arm_version = read_unsigned_int(response, 70) # 30036
177
+ self.dsp_svn_version = read_unsigned_int(response, 72) # 35037
178
+ self.arm_svn_version = read_unsigned_int(response, 74) # 35038
167
179
  self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version)
168
180
 
169
181
  if is_single_phase(self):
@@ -182,8 +194,17 @@ class DT(Inverter):
182
194
  pass
183
195
 
184
196
  async def read_runtime_data(self) -> Dict[str, Any]:
185
- response = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA)
197
+ response = await self._read_from_socket(self._READ_RUNNING_DATA)
186
198
  data = self._map_response(response, self._sensors)
199
+
200
+ if self._has_meter:
201
+ try:
202
+ response = await self._read_from_socket(self._READ_METER_DATA)
203
+ data.update(self._map_response(response, self._sensors_meter))
204
+ except (RequestRejectedException, RequestFailedException):
205
+ logger.info("Meter values not supported, disabling further attempts.")
206
+ self._has_meter = False
207
+
187
208
  return data
188
209
 
189
210
  async def read_setting(self, setting_id: str) -> Any:
@@ -263,7 +284,10 @@ class DT(Inverter):
263
284
  raise InverterError("Operation not supported, inverter has no batteries.")
264
285
 
265
286
  def sensors(self) -> Tuple[Sensor, ...]:
266
- return self._sensors
287
+ result = self._sensors
288
+ if self._has_meter:
289
+ result = result + self._sensors_meter
290
+ return result
267
291
 
268
292
  def settings(self) -> Tuple[Sensor, ...]:
269
293
  return tuple(self._settings.values())
@@ -37,7 +37,7 @@ class InverterProtocol:
37
37
  self._timer: asyncio.TimerHandle | None = None
38
38
  self.timeout: int = timeout
39
39
  self.retries: int = retries
40
- self.keep_alive: bool = True
40
+ self.keep_alive: bool = False
41
41
  self.protocol: asyncio.Protocol | None = None
42
42
  self.response_future: Future | None = None
43
43
  self.command: ProtocolCommand | None = None
@@ -62,6 +62,24 @@ class InverterProtocol:
62
62
  self._close_transport()
63
63
  return self._lock
64
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()
82
+
65
83
  async def close(self) -> None:
66
84
  """Close the underlying transport/connection."""
67
85
  raise NotImplementedError()
@@ -133,15 +151,16 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
133
151
  self._partial_missing = 0
134
152
  if self.command.validator(data):
135
153
  logger.debug("Received: %s", data.hex())
154
+ self._retry = 0
136
155
  self.response_future.set_result(data)
137
156
  else:
138
157
  logger.debug("Received invalid response: %s", data.hex())
139
- asyncio.get_running_loop().call_soon(self._retry_mechanism)
158
+ asyncio.get_running_loop().call_soon(self._timeout_mechanism)
140
159
  except PartialResponseException as ex:
141
160
  logger.debug("Received response fragment (%d of %d): %s", ex.length, ex.expected, data.hex())
142
161
  self._partial_data = data
143
162
  self._partial_missing = ex.expected - ex.length
144
- self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
163
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
145
164
  except asyncio.InvalidStateError:
146
165
  logger.debug("Response already handled: %s", data.hex())
147
166
  except RequestRejectedException as ex:
@@ -158,13 +177,28 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
158
177
 
159
178
  async def send_request(self, command: ProtocolCommand) -> Future:
160
179
  """Send message via transport"""
161
- async with self._ensure_lock():
180
+ await self._ensure_lock().acquire()
181
+ try:
162
182
  await self._connect()
163
183
  response_future = asyncio.get_running_loop().create_future()
164
- self._retry = 0
165
184
  self._send_request(command, response_future)
166
185
  await response_future
167
186
  return response_future
187
+ except asyncio.CancelledError:
188
+ if self._retry < self.retries:
189
+ self._retry += 1
190
+ if self._lock and self._lock.locked():
191
+ self._lock.release()
192
+ if not self.keep_alive:
193
+ self._close_transport()
194
+ return await self.send_request(command)
195
+ else:
196
+ return self._max_retries_reached()
197
+ finally:
198
+ if self._lock and self._lock.locked():
199
+ self._lock.release()
200
+ if not self.keep_alive:
201
+ self._close_transport()
168
202
 
169
203
  def _send_request(self, command: ProtocolCommand, response_future: Future) -> None:
170
204
  """Send message via transport"""
@@ -178,32 +212,19 @@ class UdpInverterProtocol(InverterProtocol, asyncio.DatagramProtocol):
178
212
  else:
179
213
  logger.debug("Sending: %s", self.command)
180
214
  self._transport.sendto(payload)
181
- self._timer = asyncio.get_running_loop().call_later(self.timeout, self._retry_mechanism)
215
+ self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
182
216
 
183
- def _retry_mechanism(self) -> None:
184
- """Retry mechanism to prevent hanging transport"""
185
- if self.response_future.done():
217
+ def _timeout_mechanism(self) -> None:
218
+ """Timeout mechanism to prevent hanging transport"""
219
+ if self.response_future and self.response_future.done():
186
220
  logger.debug("Response already received.")
187
- elif self._retry < self.retries:
221
+ self._retry = 0
222
+ else:
188
223
  if self._timer:
189
224
  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()
225
+ self._timer = None
226
+ if self.response_future and not self.response_future.done():
227
+ self.response_future.cancel()
207
228
 
208
229
  async def close(self):
209
230
  self._close_transport()
@@ -358,24 +379,6 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
358
379
  self._timer = None
359
380
  self._close_transport()
360
381
 
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
382
  async def close(self):
380
383
  await self._ensure_lock().acquire()
381
384
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.7
3
+ Version: 0.4.8
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
@@ -4,7 +4,8 @@ from datetime import datetime
4
4
  from unittest import TestCase
5
5
 
6
6
  from goodwe.dt import DT
7
- from goodwe.exceptions import RequestFailedException
7
+ from goodwe.exceptions import RequestFailedException, RequestRejectedException
8
+ from goodwe.modbus import ILLEGAL_DATA_ADDRESS
8
9
  from goodwe.protocol import ProtocolCommand, ProtocolResponse
9
10
 
10
11
 
@@ -13,7 +14,7 @@ class DtMock(TestCase, DT):
13
14
  def __init__(self, methodName='runTest', port=8899):
14
15
  TestCase.__init__(self, methodName)
15
16
  DT.__init__(self, "localhost", port)
16
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
17
+ self.sensor_map = {s.id_: s for s in self.sensors()}
17
18
  self._mock_responses = {}
18
19
 
19
20
  def mock_response(self, command: ProtocolCommand, filename: str):
@@ -24,6 +25,10 @@ class DtMock(TestCase, DT):
24
25
  root_dir = os.path.dirname(os.path.abspath(__file__))
25
26
  filename = self._mock_responses.get(command)
26
27
  if filename is not None:
28
+ if ILLEGAL_DATA_ADDRESS == filename:
29
+ raise RequestRejectedException(ILLEGAL_DATA_ADDRESS)
30
+ if 'NO RESPONSE' == filename:
31
+ raise RequestFailedException()
27
32
  with open(root_dir + '/sample/dt/' + filename, 'r') as f:
28
33
  response = bytes.fromhex(f.read())
29
34
  if not command.validator(response):
@@ -33,10 +38,11 @@ class DtMock(TestCase, DT):
33
38
  self.request = command.request
34
39
  return ProtocolResponse(bytes.fromhex("aa557f00010203040506070809"), command)
35
40
 
36
- def assertSensor(self, sensor, expected_value, expected_unit, data):
37
- self.assertEqual(expected_value, data.get(sensor))
38
- self.assertEqual(expected_unit, self.sensor_map.get(sensor))
39
- self.sensor_map.pop(sensor)
41
+ def assertSensor(self, sensor_name, expected_value, expected_unit, data):
42
+ self.assertEqual(expected_value, data.get(sensor_name))
43
+ sensor = self.sensor_map.get(sensor_name);
44
+ self.assertEqual(expected_unit, sensor.unit)
45
+ self.sensor_map.pop(sensor_name)
40
46
 
41
47
  @classmethod
42
48
  def setUpClass(cls):
@@ -47,12 +53,15 @@ class GW6000_DT_Test(DtMock):
47
53
 
48
54
  def __init__(self, methodName='runTest'):
49
55
  DtMock.__init__(self, methodName)
50
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW6000-DT_running_data.hex')
56
+ self.mock_response(self._READ_RUNNING_DATA, 'GW6000-DT_running_data.hex')
57
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
51
58
 
52
59
  def test_GW6000_DT_runtime_data(self):
53
60
  self.loop.run_until_complete(self.read_device_info())
54
61
  data = self.loop.run_until_complete(self.read_runtime_data())
55
- self.assertEqual(41, len(data))
62
+ self.assertEqual(42, len(data))
63
+
64
+ self.sensor_map = {s.id_: s for s in self.sensors()}
56
65
 
57
66
  self.assertSensor('timestamp', datetime.strptime('2021-08-31 12:03:02', '%Y-%m-%d %H:%M:%S'), '', data)
58
67
  self.assertSensor('vpv1', 320.8, 'V', data)
@@ -61,9 +70,6 @@ class GW6000_DT_Test(DtMock):
61
70
  self.assertSensor('vpv2', 324.1, 'V', data)
62
71
  self.assertSensor('ipv2', 3.2, 'A', data)
63
72
  self.assertSensor('ppv2', 1037, 'W', data)
64
- self.assertSensor('vpv3', None, 'V', data)
65
- self.assertSensor('ipv3', None, 'A', data)
66
- self.assertSensor('ppv3', None, 'W', data)
67
73
  self.assertSensor('ppv', 2031, 'W', data)
68
74
  self.assertSensor('vline1', 0, 'V', data)
69
75
  self.assertSensor('vline2', 0, 'V', data)
@@ -80,13 +86,14 @@ class GW6000_DT_Test(DtMock):
80
86
  self.assertSensor('pgrid1', 609, 'W', data)
81
87
  self.assertSensor('pgrid2', 597, 'W', data)
82
88
  self.assertSensor('pgrid3', 624, 'W', data)
83
- self.assertSensor('active_power', 1835, 'W', data)
89
+ self.assertSensor('total_inverter_power', 1835, 'W', data)
84
90
  self.assertSensor('work_mode', 1, '', data)
85
91
  self.assertSensor('work_mode_label', 'Normal', '', data)
86
92
  self.assertSensor('error_codes', 0, '', data)
87
93
  self.assertSensor('warning_code', 0, '', data)
88
94
  self.assertSensor("apparent_power", -1, "VA", data),
89
95
  self.assertSensor("reactive_power", -1, "var", data),
96
+ self.assertSensor("power_factor", 0.0, "", data),
90
97
  self.assertSensor('temperature', 41.3, 'C', data)
91
98
  self.assertSensor('e_day', 6.0, 'kWh', data)
92
99
  self.assertSensor('e_total', 13350.2, 'kWh', data)
@@ -121,8 +128,9 @@ class GW8K_DT_Test(DtMock):
121
128
 
122
129
  def __init__(self, methodName='runTest'):
123
130
  DtMock.__init__(self, methodName)
124
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW8K-DT_running_data.hex')
125
131
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW8K-DT_device_info.hex')
132
+ self.mock_response(self._READ_RUNNING_DATA, 'GW8K-DT_running_data.hex')
133
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
126
134
 
127
135
  def test_GW8K_DT_device_info(self):
128
136
  self.loop.run_until_complete(self.read_device_info())
@@ -130,13 +138,15 @@ class GW8K_DT_Test(DtMock):
130
138
  self.assertEqual('00000DTS00000000', self.serial_number)
131
139
  self.assertEqual(1010, self.dsp1_version)
132
140
  self.assertEqual(1010, self.dsp2_version)
141
+ self.assertEqual(728, self.dsp_svn_version)
133
142
  self.assertEqual(8, self.arm_version)
143
+ self.assertEqual(49, self.arm_svn_version)
134
144
  self.assertEqual('1010.1010.08', self.firmware)
135
145
 
136
146
  def test_GW8K_DT_runtime_data(self):
137
147
  self.loop.run_until_complete(self.read_device_info())
138
148
  data = self.loop.run_until_complete(self.read_runtime_data())
139
- self.assertEqual(41, len(data))
149
+ self.assertEqual(42, len(data))
140
150
 
141
151
  self.assertSensor('timestamp', datetime.strptime('2021-08-24 16:43:27', '%Y-%m-%d %H:%M:%S'), '', data)
142
152
  self.assertSensor('vpv1', 275.5, 'V', data)
@@ -161,13 +171,14 @@ class GW8K_DT_Test(DtMock):
161
171
  self.assertSensor('pgrid1', 237, 'W', data)
162
172
  self.assertSensor('pgrid2', 240, 'W', data)
163
173
  self.assertSensor('pgrid3', 235, 'W', data)
164
- self.assertSensor('active_power', 643, 'W', data)
174
+ self.assertSensor('total_inverter_power', 643, 'W', data)
165
175
  self.assertSensor('work_mode', 1, '', data)
166
176
  self.assertSensor('work_mode_label', 'Normal', '', data)
167
177
  self.assertSensor('error_codes', 0, '', data)
168
178
  self.assertSensor('warning_code', 0, '', data)
169
179
  self.assertSensor("apparent_power", 0, "VA", data),
170
180
  self.assertSensor("reactive_power", 0, "var", data),
181
+ self.assertSensor("power_factor", 0.0, "", data),
171
182
  self.assertSensor('temperature', 45.3, 'C', data)
172
183
  self.assertSensor('e_day', None, 'kWh', data)
173
184
  self.assertSensor('e_total', None, 'kWh', data)
@@ -195,13 +206,14 @@ class GW5000D_NS_Test(DtMock):
195
206
 
196
207
  def __init__(self, methodName='runTest'):
197
208
  DtMock.__init__(self, methodName)
198
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5000D-NS_running_data.hex')
199
209
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'Mock_device_info.hex')
210
+ self.mock_response(self._READ_RUNNING_DATA, 'GW5000D-NS_running_data.hex')
211
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
200
212
 
201
213
  def test_GW5000D_NS_runtime_data(self):
202
214
  self.loop.run_until_complete(self.read_device_info())
203
215
  data = self.loop.run_until_complete(self.read_runtime_data())
204
- self.assertEqual(31, len(data))
216
+ self.assertEqual(32, len(data))
205
217
 
206
218
  self.assertSensor('timestamp', datetime.strptime('2021-09-06 06:56:01', '%Y-%m-%d %H:%M:%S'), '', data)
207
219
  self.assertSensor('vpv1', 224.4, 'V', data)
@@ -216,13 +228,14 @@ class GW5000D_NS_Test(DtMock):
216
228
  self.assertSensor('igrid1', 0.0, 'A', data)
217
229
  self.assertSensor('fgrid1', 49.97, 'Hz', data)
218
230
  self.assertSensor('pgrid1', 0, 'W', data)
219
- self.assertSensor('active_power', 0, 'W', data)
231
+ self.assertSensor('total_inverter_power', 0, 'W', data)
220
232
  self.assertSensor('work_mode', 0, '', data)
221
233
  self.assertSensor('work_mode_label', 'Wait Mode', '', data)
222
234
  self.assertSensor('error_codes', 0, '', data)
223
235
  self.assertSensor('warning_code', 0, '', data)
224
236
  self.assertSensor("apparent_power", -1, "VA", data),
225
237
  self.assertSensor("reactive_power", -1, "var", data),
238
+ self.assertSensor("power_factor", -0.001, "", data),
226
239
  self.assertSensor('temperature', 1.4, 'C', data)
227
240
  self.assertSensor('e_day', 0, 'kWh', data)
228
241
  self.assertSensor('e_total', 881.7, 'kWh', data)
@@ -250,8 +263,9 @@ class GW5000_MS_Test(DtMock):
250
263
 
251
264
  def __init__(self, methodName='runTest'):
252
265
  DtMock.__init__(self, methodName)
253
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5000-MS_running_data.hex')
254
266
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW5000-MS_device_info.hex')
267
+ self.mock_response(self._READ_RUNNING_DATA, 'GW5000-MS_running_data.hex')
268
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
255
269
 
256
270
  def test_GW6000_MS_device_info(self):
257
271
  self.loop.run_until_complete(self.read_device_info())
@@ -259,13 +273,15 @@ class GW5000_MS_Test(DtMock):
259
273
  self.assertEqual('00000MSU00000000', self.serial_number)
260
274
  self.assertEqual(12, self.dsp1_version)
261
275
  self.assertEqual(12, self.dsp2_version)
276
+ self.assertEqual(65535, self.dsp_svn_version)
262
277
  self.assertEqual(16, self.arm_version)
278
+ self.assertEqual(271, self.arm_svn_version)
263
279
  self.assertEqual('12.12.10', self.firmware)
264
280
 
265
281
  def test_GW5000_MS_runtime_data(self):
266
282
  self.loop.run_until_complete(self.read_device_info())
267
283
  data = self.loop.run_until_complete(self.read_runtime_data())
268
- self.assertEqual(34, len(data))
284
+ self.assertEqual(35, len(data))
269
285
 
270
286
  self.assertSensor('timestamp', datetime.strptime('2021-10-15 09:03:12', '%Y-%m-%d %H:%M:%S'), '', data)
271
287
  self.assertSensor('vpv1', 319.6, 'V', data)
@@ -283,13 +299,14 @@ class GW5000_MS_Test(DtMock):
283
299
  self.assertSensor('igrid1', 0.9, 'A', data)
284
300
  self.assertSensor('fgrid1', 49.98, 'Hz', data)
285
301
  self.assertSensor('pgrid1', 216, 'W', data)
286
- self.assertSensor('active_power', 295, 'W', data)
302
+ self.assertSensor('total_inverter_power', 295, 'W', data)
287
303
  self.assertSensor('work_mode', 1, '', data)
288
304
  self.assertSensor('work_mode_label', 'Normal', '', data)
289
305
  self.assertSensor('error_codes', 0, '', data)
290
306
  self.assertSensor('warning_code', 0, '', data)
291
307
  self.assertSensor("apparent_power", -1, "VA", data),
292
308
  self.assertSensor("reactive_power", -1, "var", data),
309
+ self.assertSensor("power_factor", -0.001, "", data),
293
310
  self.assertSensor('temperature', 10.7, 'C', data)
294
311
  self.assertSensor('e_day', 0.4, 'kWh', data)
295
312
  self.assertSensor('e_total', 6.8, 'kWh', data)
@@ -308,7 +325,8 @@ class GW10K_MS_30_Test(DtMock):
308
325
  def __init__(self, methodName='runTest'):
309
326
  DtMock.__init__(self, methodName)
310
327
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW10K-MS-30_device_info.hex')
311
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW10K-MS-30_running_data.hex')
328
+ self.mock_response(self._READ_RUNNING_DATA, 'GW10K-MS-30_running_data.hex')
329
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
312
330
 
313
331
  def test_GW10K_MS_30_device_info(self):
314
332
  self.loop.run_until_complete(self.read_device_info())
@@ -316,13 +334,15 @@ class GW10K_MS_30_Test(DtMock):
316
334
  self.assertEqual('5010KMSC000W0000', self.serial_number)
317
335
  self.assertEqual(0, self.dsp1_version)
318
336
  self.assertEqual(0, self.dsp2_version)
337
+ self.assertEqual(504, self.dsp_svn_version)
319
338
  self.assertEqual(2, self.arm_version)
339
+ self.assertEqual(13, self.arm_svn_version)
320
340
  self.assertEqual('0.0.02', self.firmware)
321
341
 
322
342
  def test_GW10K_MS_30_runtime_data(self):
323
343
  self.loop.run_until_complete(self.read_device_info())
324
344
  data = self.loop.run_until_complete(self.read_runtime_data())
325
- self.assertEqual(34, len(data))
345
+ self.assertEqual(35, len(data))
326
346
 
327
347
  self.assertSensor('timestamp', datetime.strptime('2024-01-09 22:08:20', '%Y-%m-%d %H:%M:%S'), '', data)
328
348
  self.assertSensor('vpv1', 0.0, 'V', data)
@@ -340,13 +360,14 @@ class GW10K_MS_30_Test(DtMock):
340
360
  self.assertSensor('igrid1', 0.0, 'A', data)
341
361
  self.assertSensor('fgrid1', 50.0, 'Hz', data)
342
362
  self.assertSensor('pgrid1', 0, 'W', data)
343
- self.assertSensor('active_power', 0, 'W', data)
363
+ self.assertSensor('total_inverter_power', 0, 'W', data)
344
364
  self.assertSensor('work_mode', 0, '', data)
345
365
  self.assertSensor('work_mode_label', 'Wait Mode', '', data)
346
366
  self.assertSensor('error_codes', 0, '', data)
347
367
  self.assertSensor('warning_code', 0, '', data)
348
368
  self.assertSensor("apparent_power", 0, "VA", data),
349
369
  self.assertSensor("reactive_power", 0, "var", data),
370
+ self.assertSensor("power_factor", 0.0, "", data),
350
371
  self.assertSensor('temperature', 24.3, 'C', data)
351
372
  self.assertSensor('e_day', 71.8, 'kWh', data)
352
373
  self.assertSensor('e_total', 3433.4, 'kWh', data)
@@ -364,12 +385,13 @@ class GW10K_MS_TCP_Test(DtMock):
364
385
 
365
386
  def __init__(self, methodName='runTest'):
366
387
  DtMock.__init__(self, methodName, 502)
367
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW10K-MS-30_tcp_running_data.hex')
388
+ self.mock_response(self._READ_RUNNING_DATA, 'GW10K-MS-30_tcp_running_data.hex')
389
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
368
390
 
369
391
  def test_GW10K_MS_TCP_runtime_data(self):
370
392
  self.loop.run_until_complete(self.read_device_info())
371
393
  data = self.loop.run_until_complete(self.read_runtime_data())
372
- self.assertEqual(41, len(data))
394
+ self.assertEqual(42, len(data))
373
395
 
374
396
  self.assertSensor('timestamp', datetime.strptime('2024-06-02 09:07:17', '%Y-%m-%d %H:%M:%S'), '', data)
375
397
  self.assertSensor('vpv1', 400.6, 'V', data)
@@ -394,13 +416,14 @@ class GW10K_MS_TCP_Test(DtMock):
394
416
  self.assertSensor('pgrid1', 5955, 'W', data)
395
417
  self.assertSensor('pgrid2', 0, 'W', data)
396
418
  self.assertSensor('pgrid3', 0, 'W', data)
397
- self.assertSensor('active_power', 5914, 'W', data)
419
+ self.assertSensor('total_inverter_power', 5914, 'W', data)
398
420
  self.assertSensor('work_mode', 1, '', data)
399
421
  self.assertSensor('work_mode_label', 'Normal', '', data)
400
422
  self.assertSensor('error_codes', 0, '', data)
401
423
  self.assertSensor('warning_code', 0, '', data)
402
424
  self.assertSensor('apparent_power', 5957, 'VA', data)
403
425
  self.assertSensor('reactive_power', -6, 'var', data)
426
+ self.assertSensor("power_factor", 0.999, "", data),
404
427
  self.assertSensor('temperature', 36.0, 'C', data)
405
428
  self.assertSensor('e_day', 4.3, 'kWh', data)
406
429
  self.assertSensor('e_total', 998.2, 'kWh', data)
@@ -418,8 +441,9 @@ class GW20KAU_DT_Test(DtMock):
418
441
 
419
442
  def __init__(self, methodName='runTest'):
420
443
  DtMock.__init__(self, methodName)
421
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW20KAU-DT_running_data.hex')
422
444
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW20KAU-DT_device_info.hex')
445
+ self.mock_response(self._READ_RUNNING_DATA, 'GW20KAU-DT_running_data.hex')
446
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
423
447
 
424
448
  def test_GW20KAU_DT_device_info(self):
425
449
  self.loop.run_until_complete(self.read_device_info())
@@ -427,13 +451,15 @@ class GW20KAU_DT_Test(DtMock):
427
451
  self.assertEqual('0000KDTA00000000', self.serial_number)
428
452
  self.assertEqual(15, self.dsp1_version)
429
453
  self.assertEqual(15, self.dsp2_version)
454
+ self.assertEqual(1099, self.dsp_svn_version)
430
455
  self.assertEqual(16, self.arm_version)
456
+ self.assertEqual(187, self.arm_svn_version)
431
457
  self.assertEqual('15.15.10', self.firmware)
432
458
 
433
459
  def test_GW20KAU_DT_runtime_data(self):
434
460
  self.loop.run_until_complete(self.read_device_info())
435
461
  data = self.loop.run_until_complete(self.read_runtime_data())
436
- self.assertEqual(41, len(data))
462
+ self.assertEqual(42, len(data))
437
463
 
438
464
  self.assertSensor('timestamp', datetime.strptime('2022-10-21 19:23:42', '%Y-%m-%d %H:%M:%S'), '', data)
439
465
  self.assertSensor('vpv1', 390.5, 'V', data)
@@ -458,13 +484,14 @@ class GW20KAU_DT_Test(DtMock):
458
484
  self.assertSensor('pgrid1', 1628, 'W', data)
459
485
  self.assertSensor('pgrid2', 1655, 'W', data)
460
486
  self.assertSensor('pgrid3', 1621, 'W', data)
461
- self.assertSensor('active_power', 4957, 'W', data)
487
+ self.assertSensor('total_inverter_power', 4957, 'W', data)
462
488
  self.assertSensor('work_mode', 1, '', data)
463
489
  self.assertSensor('work_mode_label', 'Normal', '', data)
464
490
  self.assertSensor('error_codes', 0, '', data)
465
491
  self.assertSensor('warning_code', 0, '', data)
466
492
  self.assertSensor("apparent_power", 0, "VA", data),
467
493
  self.assertSensor("reactive_power", 205, "var", data),
494
+ self.assertSensor("power_factor", 0.999, "", data),
468
495
  self.assertSensor('temperature', 36.4, 'C', data)
469
496
  self.assertSensor('e_day', 19.8, 'kWh', data)
470
497
  self.assertSensor('e_total', 4304.8, 'kWh', data)
@@ -482,8 +509,9 @@ class GW17K_DT_Test(DtMock):
482
509
 
483
510
  def __init__(self, methodName='runTest'):
484
511
  DtMock.__init__(self, methodName)
485
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW17K-DT_running_data.hex')
486
512
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW17K-DT_device_info.hex')
513
+ self.mock_response(self._READ_RUNNING_DATA, 'GW17K-DT_running_data.hex')
514
+ self.mock_response(self._READ_METER_DATA, ILLEGAL_DATA_ADDRESS)
487
515
 
488
516
  def test_GW20KAU_DT_device_info(self):
489
517
  self.loop.run_until_complete(self.read_device_info())
@@ -491,13 +519,15 @@ class GW17K_DT_Test(DtMock):
491
519
  self.assertEqual('5017KDTT00BW0000', self.serial_number)
492
520
  self.assertEqual(12, self.dsp1_version)
493
521
  self.assertEqual(12, self.dsp2_version)
522
+ self.assertEqual(931, self.dsp_svn_version)
494
523
  self.assertEqual(13, self.arm_version)
524
+ self.assertEqual(130, self.arm_svn_version)
495
525
  self.assertEqual('12.12.0d', self.firmware)
496
526
 
497
527
  def test_GW20KAU_DT_runtime_data(self):
498
528
  self.loop.run_until_complete(self.read_device_info())
499
529
  data = self.loop.run_until_complete(self.read_runtime_data())
500
- self.assertEqual(41, len(data))
530
+ self.assertEqual(42, len(data))
501
531
 
502
532
  self.assertSensor('timestamp', datetime.strptime('2024-05-20 10:35:55', '%Y-%m-%d %H:%M:%S'), '', data)
503
533
  self.assertSensor('vpv1', 540.0, 'V', data)
@@ -522,13 +552,14 @@ class GW17K_DT_Test(DtMock):
522
552
  self.assertSensor('pgrid1', 4166, 'W', data)
523
553
  self.assertSensor('pgrid2', 4170, 'W', data)
524
554
  self.assertSensor('pgrid3', 4153, 'W', data)
525
- self.assertSensor('active_power', 12470, 'W', data)
555
+ self.assertSensor('total_inverter_power', 12470, 'W', data)
526
556
  self.assertSensor('work_mode', 1, '', data)
527
557
  self.assertSensor('work_mode_label', 'Normal', '', data)
528
558
  self.assertSensor('error_codes', 0, '', data)
529
559
  self.assertSensor('warning_code', 0, '', data)
530
560
  self.assertSensor('apparent_power', 0, 'VA', data)
531
561
  self.assertSensor('reactive_power', 0, 'var', data)
562
+ self.assertSensor("power_factor", 0.0, "", data),
532
563
  self.assertSensor('temperature', 45.7, 'C', data)
533
564
  self.assertSensor('e_day', 29.3, 'kWh', data)
534
565
  self.assertSensor('e_total', 29984.4, 'kWh', data)
@@ -36,14 +36,14 @@ class TestUDPClientProtocol(TestCase):
36
36
  mock_loop = mock.Mock()
37
37
  mock_get_event_loop.return_value = mock_loop
38
38
 
39
- mock_retry_mechanism = mock.Mock()
40
- self.protocol._retry_mechanism = mock_retry_mechanism
39
+ mock_timeout_mechanism = mock.Mock()
40
+ self.protocol._timeout_mechanism = mock_timeout_mechanism
41
41
  self.protocol.connection_made(transport)
42
42
  self.protocol._send_request(self.protocol.command, self.protocol.response_future)
43
43
 
44
44
  transport.sendto.assert_called_with(self.protocol.command.request)
45
45
  mock_get_event_loop.assert_called()
46
- mock_loop.call_later.assert_called_with(1, mock_retry_mechanism)
46
+ mock_loop.call_later.assert_called_with(1, mock_timeout_mechanism)
47
47
 
48
48
  def test_connection_lost(self):
49
49
  self.protocol.response_future.done.return_value = True
@@ -59,41 +59,41 @@ class TestUDPClientProtocol(TestCase):
59
59
  self.protocol._transport = mock.Mock()
60
60
  self.protocol._send_request = mock.Mock()
61
61
  self.protocol.response_future.done.return_value = True
62
- self.protocol._retry_mechanism()
62
+ self.protocol._timeout_mechanism()
63
63
 
64
64
  # self.protocol._transport.close.assert_called()
65
65
  self.protocol._send_request.assert_not_called()
66
66
 
67
- @mock.patch('goodwe.protocol.asyncio.get_running_loop')
68
- def test_retry_mechanism_two_retries(self, mock_get_event_loop):
69
- def call_later(_: int, retry_func: Callable):
70
- retry_func()
71
-
72
- mock_loop = mock.Mock()
73
- mock_get_event_loop.return_value = mock_loop
74
- mock_loop.call_later = call_later
75
-
76
- self.protocol._transport = mock.Mock()
77
- self.protocol.response_future.done.side_effect = [False, False, True, False]
78
- self.protocol._retry_mechanism()
79
-
80
- # self.protocol._transport.close.assert_called()
81
- self.assertEqual(self.protocol._retry, 2)
82
-
83
- @mock.patch('goodwe.protocol.asyncio.get_running_loop')
84
- def test_retry_mechanism_max_retries(self, mock_get_event_loop):
85
- def call_later(_: int, retry_func: Callable):
86
- retry_func()
87
-
88
- mock_loop = mock.Mock()
89
- mock_get_event_loop.return_value = mock_loop
90
- mock_loop.call_later = call_later
91
-
92
- self.protocol._transport = mock.Mock()
93
- self.protocol.response_future.done.side_effect = [False, False, False, False, False]
94
- self.protocol._retry_mechanism()
95
- self.protocol.response_future.set_exception.assert_called_once_with(MaxRetriesException)
96
- self.assertEqual(self.protocol._retry, 3)
67
+ # @mock.patch('goodwe.protocol.asyncio.get_running_loop')
68
+ # def test_retry_mechanism_two_retries(self, mock_get_event_loop):
69
+ # def call_later(_: int, retry_func: Callable):
70
+ # retry_func()
71
+ #
72
+ # mock_loop = mock.Mock()
73
+ # mock_get_event_loop.return_value = mock_loop
74
+ # mock_loop.call_later = call_later
75
+ #
76
+ # self.protocol._transport = mock.Mock()
77
+ # self.protocol.response_future.done.side_effect = [False, False, True, False]
78
+ # self.protocol._timeout_mechanism()
79
+ #
80
+ # # self.protocol._transport.close.assert_called()
81
+ # self.assertEqual(self.protocol._retry, 2)
82
+
83
+ # @mock.patch('goodwe.protocol.asyncio.get_running_loop')
84
+ # def test_retry_mechanism_max_retries(self, mock_get_event_loop):
85
+ # def call_later(_: int, retry_func: Callable):
86
+ # retry_func()
87
+ #
88
+ # mock_loop = mock.Mock()
89
+ # mock_get_event_loop.return_value = mock_loop
90
+ # mock_loop.call_later = call_later
91
+ #
92
+ # self.protocol._transport = mock.Mock()
93
+ # self.protocol.response_future.done.side_effect = [False, False, False, False, False]
94
+ # self.protocol._timeout_mechanism()
95
+ # self.protocol.response_future.set_exception.assert_called_once_with(MaxRetriesException)
96
+ # self.assertEqual(self.protocol._retry, 3)
97
97
 
98
98
  def test_modbus_rtu_read_command(self):
99
99
  command = ModbusRtuReadCommand(0xf7, 0x88b8, 0x0021)
goodwe-0.4.7/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.4.7
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