goodwe 0.4.6__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.6/goodwe.egg-info → goodwe-0.4.8}/PKG-INFO +1 -1
  2. goodwe-0.4.8/VERSION +1 -0
  3. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/dt.py +40 -10
  4. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/et.py +35 -3
  5. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/model.py +4 -0
  6. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/protocol.py +48 -45
  7. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/sensor.py +19 -0
  8. {goodwe-0.4.6 → goodwe-0.4.8/goodwe.egg-info}/PKG-INFO +1 -1
  9. {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_dt.py +124 -32
  10. {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_et.py +20 -37
  11. {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_protocol.py +34 -34
  12. {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_sensor.py +10 -0
  13. goodwe-0.4.6/VERSION +0 -1
  14. {goodwe-0.4.6 → goodwe-0.4.8}/LICENSE +0 -0
  15. {goodwe-0.4.6 → goodwe-0.4.8}/README.md +0 -0
  16. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/__init__.py +0 -0
  17. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/const.py +0 -0
  18. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/es.py +0 -0
  19. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/exceptions.py +0 -0
  20. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/inverter.py +0 -0
  21. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/modbus.py +0 -0
  22. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe.egg-info/SOURCES.txt +0 -0
  23. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe.egg-info/dependency_links.txt +0 -0
  24. {goodwe-0.4.6 → goodwe-0.4.8}/goodwe.egg-info/top_level.txt +0 -0
  25. {goodwe-0.4.6 → goodwe-0.4.8}/pyproject.toml +0 -0
  26. {goodwe-0.4.6 → goodwe-0.4.8}/setup.cfg +0 -0
  27. {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_es.py +0 -0
  28. {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_modbus.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.6
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
@@ -35,6 +35,12 @@ class DT(Inverter):
35
35
  Calculated("ppv3",
36
36
  lambda data: round(read_voltage(data, 30107) * read_current(data, 30108)),
37
37
  "PV3 Power", "W", Kind.PV),
38
+ # ppv1 + ppv2 + ppv3
39
+ Calculated("ppv",
40
+ lambda data: (round(read_voltage(data, 30103) * read_current(data, 30104))) + (round(
41
+ read_voltage(data, 30105) * read_current(data, 30106))) + (round(
42
+ read_voltage(data, 30107) * read_current(data, 30108))),
43
+ "PV Power", "W", Kind.PV),
38
44
  # Voltage("vpv4", 14, "PV4 Voltage", Kind.PV),
39
45
  # Current("ipv4", 16, "PV4 Current", Kind.PV),
40
46
  # Voltage("vpv5", 14, "PV5 Voltage", Kind.PV),
@@ -63,7 +69,7 @@ class DT(Inverter):
63
69
  lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
64
70
  "On-grid L3 Power", "W", Kind.AC),
65
71
  # 30127 reserved
66
- Power("ppv", 30128, "PV Power", Kind.PV),
72
+ PowerS("total_inverter_power", 30128, "Total Power", Kind.AC),
67
73
  Integer("work_mode", 30129, "Work Mode code"),
68
74
  Enum2("work_mode_label", 30129, WORK_MODES, "Work Mode"),
69
75
  Long("error_codes", 30130, "Error Codes"),
@@ -72,7 +78,7 @@ class DT(Inverter):
72
78
  Reactive4("reactive_power", 30135, "Reactive Power", Kind.AC),
73
79
  # 30137 reserved
74
80
  # 30138 reserved
75
- # 30139 reserved
81
+ Decimal("power_factor", 30139, 1000, "Power Factor", "", Kind.GRID),
76
82
  # 30140 reserved
77
83
  Temp("temperature", 30141, "Inverter Temperature", Kind.AC),
78
84
  # 30142 reserved
@@ -107,6 +113,12 @@ class DT(Inverter):
107
113
  # 30172 reserved
108
114
  )
109
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
+
110
122
  # Modbus registers of inverter settings, offsets are modbus register addresses
111
123
  __all_settings: Tuple[Sensor, ...] = (
112
124
  Timestamp("time", 40313, "Inverter time"),
@@ -133,9 +145,12 @@ class DT(Inverter):
133
145
  def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
134
146
  super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
135
147
  self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
136
- 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)
137
150
  self._sensors = self.__all_sensors
151
+ self._sensors_meter = self.__all_sensors_meter
138
152
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
153
+ self._has_meter: bool = True
139
154
 
140
155
  @staticmethod
141
156
  def _single_phase_only(s: Sensor) -> bool:
@@ -154,10 +169,13 @@ class DT(Inverter):
154
169
  self.model_name = response[22:32].decode("ascii").rstrip()
155
170
  except:
156
171
  print("No model name sent from the inverter.")
157
- self.serial_number = self._decode(response[6:22])
158
- self.dsp1_version = read_unsigned_int(response, 66)
159
- self.dsp2_version = read_unsigned_int(response, 68)
160
- 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
161
179
  self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version)
162
180
 
163
181
  if is_single_phase(self):
@@ -176,8 +194,17 @@ class DT(Inverter):
176
194
  pass
177
195
 
178
196
  async def read_runtime_data(self) -> Dict[str, Any]:
179
- response = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA)
197
+ response = await self._read_from_socket(self._READ_RUNNING_DATA)
180
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
+
181
208
  return data
182
209
 
183
210
  async def read_setting(self, setting_id: str) -> Any:
@@ -257,7 +284,10 @@ class DT(Inverter):
257
284
  raise InverterError("Operation not supported, inverter has no batteries.")
258
285
 
259
286
  def sensors(self) -> Tuple[Sensor, ...]:
260
- return self._sensors
287
+ result = self._sensors
288
+ if self._has_meter:
289
+ result = result + self._sensors_meter
290
+ return result
261
291
 
262
292
  def settings(self) -> Tuple[Sensor, ...]:
263
293
  return tuple(self._settings.values())
@@ -257,7 +257,8 @@ class ET(Inverter):
257
257
  Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
258
258
  Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
259
259
  Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
260
- # Sensors added in some ARM fw update, read when flag _has_meter_extended is on
260
+
261
+ # Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on
261
262
  Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
262
263
  Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
263
264
  Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
@@ -268,6 +269,15 @@ class ET(Inverter):
268
269
  Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
269
270
  Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
270
271
  Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID),
272
+
273
+ Energy8("meter_e_total_exp1", 36092, "Meter Total Energy (export) L1", Kind.GRID),
274
+ Energy8("meter_e_total_exp2", 36096, "Meter Total Energy (export) L2", Kind.GRID),
275
+ Energy8("meter_e_total_exp3", 36100, "Meter Total Energy (export) L3", Kind.GRID),
276
+ Energy8("meter_e_total_exp", 36104, "Meter Total Energy (export)", Kind.GRID),
277
+ Energy8("meter_e_total_imp1", 36108, "Meter Total Energy (import) L1", Kind.GRID),
278
+ Energy8("meter_e_total_imp2", 36112, "Meter Total Energy (import) L2", Kind.GRID),
279
+ Energy8("meter_e_total_imp3", 36116, "Meter Total Energy (import) L3", Kind.GRID),
280
+ Energy8("meter_e_total_imp", 36120, "Meter Total Energy (import)", Kind.GRID),
271
281
  )
272
282
 
273
283
  # Inverter's MPPT data
@@ -464,6 +474,7 @@ class ET(Inverter):
464
474
  self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
465
475
  self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
466
476
  self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
477
+ self._READ_METER_DATA_EXTENDED2: ProtocolCommand = self._read_command(0x8ca0, 0x7d)
467
478
  self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
468
479
  self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
469
480
  self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
@@ -472,6 +483,7 @@ class ET(Inverter):
472
483
  self._has_battery: bool = True
473
484
  self._has_battery2: bool = False
474
485
  self._has_meter_extended: bool = False
486
+ self._has_meter_extended2: bool = False
475
487
  self._has_mppt: bool = False
476
488
  self._sensors = self.__all_sensors
477
489
  self._sensors_battery = self.__all_sensors_battery
@@ -490,6 +502,11 @@ class ET(Inverter):
490
502
  """Filter to exclude extended meter sensors"""
491
503
  return s.offset < 36045
492
504
 
505
+ @staticmethod
506
+ def _not_extended_meter2(s: Sensor) -> bool:
507
+ """Filter to exclude extended meter sensors"""
508
+ return s.offset < 36058
509
+
493
510
  async def read_device_info(self):
494
511
  response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
495
512
  response = response.response_data()
@@ -520,9 +537,10 @@ class ET(Inverter):
520
537
  if is_2_battery(self) or self.rated_power >= 25000:
521
538
  self._has_battery2 = True
522
539
 
523
- if self.rated_power >= 15000:
540
+ if is_745_platform(self) or self.rated_power >= 15000:
524
541
  self._has_mppt = True
525
542
  self._has_meter_extended = True
543
+ self._has_meter_extended2 = True
526
544
  else:
527
545
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
528
546
 
@@ -577,7 +595,21 @@ class ET(Inverter):
577
595
  else:
578
596
  raise ex
579
597
 
580
- if self._has_meter_extended:
598
+ if self._has_meter_extended2:
599
+ try:
600
+ response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2)
601
+ data.update(self._map_response(response, self._sensors_meter))
602
+ except RequestRejectedException as ex:
603
+ if ex.message == ILLEGAL_DATA_ADDRESS:
604
+ logger.info("Extended meter values not supported, disabling further attempts.")
605
+ self._has_meter_extended2 = False
606
+ self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter))
607
+ response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
608
+ data.update(
609
+ self._map_response(response, self._sensors_meter))
610
+ else:
611
+ raise ex
612
+ elif self._has_meter_extended:
581
613
  try:
582
614
  response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
583
615
  data.update(self._map_response(response, self._sensors_meter))
@@ -48,3 +48,7 @@ def is_2_battery(inverter: Inverter) -> bool:
48
48
  def is_745_platform(inverter: Inverter) -> bool:
49
49
  return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any(
50
50
  model in inverter.serial_number for model in PLATFORM_745_HV_MODELS)
51
+
52
+
53
+ def is_753_platform(inverter: Inverter) -> bool:
54
+ return any(model in inverter.serial_number for model in PLATFORM_753_MODELS)
@@ -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:
@@ -197,6 +197,17 @@ class Energy4(Sensor):
197
197
  return float(value) / 10 if value is not None else None
198
198
 
199
199
 
200
+ class Energy8(Sensor):
201
+ """Sensor representing energy [kWh] value encoded in 8 bytes"""
202
+
203
+ def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]):
204
+ super().__init__(id_, offset, name, 8, "kWh", kind)
205
+
206
+ def read_value(self, data: ProtocolResponse):
207
+ value = read_bytes8(data)
208
+ return float(value) / 100 if value is not None else None
209
+
210
+
200
211
  class Apparent(Sensor):
201
212
  """Sensor representing apparent power [VA] value encoded in 2 bytes"""
202
213
 
@@ -840,6 +851,14 @@ def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
840
851
  return int.from_bytes(buffer.read(4), byteorder="big", signed=True)
841
852
 
842
853
 
854
+ def read_bytes8(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
855
+ """Retrieve 8 byte (unsigned int) value from buffer"""
856
+ if offset is not None:
857
+ buffer.seek(offset)
858
+ value = int.from_bytes(buffer.read(8), byteorder="big", signed=False)
859
+ return undef if value == 0xffffffffffffffff else value
860
+
861
+
843
862
  def read_decimal2(buffer: ProtocolResponse, scale: int, offset: int = None) -> float:
844
863
  """Retrieve 2 byte (signed float) value from buffer"""
845
864
  if offset is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.6
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,16 +4,17 @@ 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
 
11
12
  class DtMock(TestCase, DT):
12
13
 
13
- def __init__(self, methodName='runTest'):
14
+ def __init__(self, methodName='runTest', port=8899):
14
15
  TestCase.__init__(self, methodName)
15
- DT.__init__(self, "localhost", 8899)
16
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
16
+ DT.__init__(self, "localhost", port)
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(40, 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,7 @@ 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)
73
+ self.assertSensor('ppv', 2031, 'W', data)
67
74
  self.assertSensor('vline1', 0, 'V', data)
68
75
  self.assertSensor('vline2', 0, 'V', data)
69
76
  self.assertSensor('vline3', 0, 'V', data)
@@ -79,13 +86,14 @@ class GW6000_DT_Test(DtMock):
79
86
  self.assertSensor('pgrid1', 609, 'W', data)
80
87
  self.assertSensor('pgrid2', 597, 'W', data)
81
88
  self.assertSensor('pgrid3', 624, 'W', data)
82
- self.assertSensor('ppv', 1835, 'W', data)
89
+ self.assertSensor('total_inverter_power', 1835, 'W', data)
83
90
  self.assertSensor('work_mode', 1, '', data)
84
91
  self.assertSensor('work_mode_label', 'Normal', '', data)
85
92
  self.assertSensor('error_codes', 0, '', data)
86
93
  self.assertSensor('warning_code', 0, '', data)
87
94
  self.assertSensor("apparent_power", -1, "VA", data),
88
95
  self.assertSensor("reactive_power", -1, "var", data),
96
+ self.assertSensor("power_factor", 0.0, "", data),
89
97
  self.assertSensor('temperature', 41.3, 'C', data)
90
98
  self.assertSensor('e_day', 6.0, 'kWh', data)
91
99
  self.assertSensor('e_total', 13350.2, 'kWh', data)
@@ -120,8 +128,9 @@ class GW8K_DT_Test(DtMock):
120
128
 
121
129
  def __init__(self, methodName='runTest'):
122
130
  DtMock.__init__(self, methodName)
123
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW8K-DT_running_data.hex')
124
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)
125
134
 
126
135
  def test_GW8K_DT_device_info(self):
127
136
  self.loop.run_until_complete(self.read_device_info())
@@ -129,13 +138,15 @@ class GW8K_DT_Test(DtMock):
129
138
  self.assertEqual('00000DTS00000000', self.serial_number)
130
139
  self.assertEqual(1010, self.dsp1_version)
131
140
  self.assertEqual(1010, self.dsp2_version)
141
+ self.assertEqual(728, self.dsp_svn_version)
132
142
  self.assertEqual(8, self.arm_version)
143
+ self.assertEqual(49, self.arm_svn_version)
133
144
  self.assertEqual('1010.1010.08', self.firmware)
134
145
 
135
146
  def test_GW8K_DT_runtime_data(self):
136
147
  self.loop.run_until_complete(self.read_device_info())
137
148
  data = self.loop.run_until_complete(self.read_runtime_data())
138
- self.assertEqual(40, len(data))
149
+ self.assertEqual(42, len(data))
139
150
 
140
151
  self.assertSensor('timestamp', datetime.strptime('2021-08-24 16:43:27', '%Y-%m-%d %H:%M:%S'), '', data)
141
152
  self.assertSensor('vpv1', 275.5, 'V', data)
@@ -144,6 +155,7 @@ class GW8K_DT_Test(DtMock):
144
155
  self.assertSensor('vpv2', 510.8, 'V', data)
145
156
  self.assertSensor('ipv2', 0.8, 'A', data)
146
157
  self.assertSensor('ppv2', 409, 'W', data)
158
+ self.assertSensor('ppv', 574, 'W', data)
147
159
  self.assertSensor('vline1', 413.7, 'V', data)
148
160
  self.assertSensor('vline2', 413.0, 'V', data)
149
161
  self.assertSensor('vline3', 408.0, 'V', data)
@@ -159,13 +171,14 @@ class GW8K_DT_Test(DtMock):
159
171
  self.assertSensor('pgrid1', 237, 'W', data)
160
172
  self.assertSensor('pgrid2', 240, 'W', data)
161
173
  self.assertSensor('pgrid3', 235, 'W', data)
162
- self.assertSensor('ppv', 643, 'W', data)
174
+ self.assertSensor('total_inverter_power', 643, 'W', data)
163
175
  self.assertSensor('work_mode', 1, '', data)
164
176
  self.assertSensor('work_mode_label', 'Normal', '', data)
165
177
  self.assertSensor('error_codes', 0, '', data)
166
178
  self.assertSensor('warning_code', 0, '', data)
167
179
  self.assertSensor("apparent_power", 0, "VA", data),
168
180
  self.assertSensor("reactive_power", 0, "var", data),
181
+ self.assertSensor("power_factor", 0.0, "", data),
169
182
  self.assertSensor('temperature', 45.3, 'C', data)
170
183
  self.assertSensor('e_day', None, 'kWh', data)
171
184
  self.assertSensor('e_total', None, 'kWh', data)
@@ -193,13 +206,14 @@ class GW5000D_NS_Test(DtMock):
193
206
 
194
207
  def __init__(self, methodName='runTest'):
195
208
  DtMock.__init__(self, methodName)
196
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5000D-NS_running_data.hex')
197
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)
198
212
 
199
213
  def test_GW5000D_NS_runtime_data(self):
200
214
  self.loop.run_until_complete(self.read_device_info())
201
215
  data = self.loop.run_until_complete(self.read_runtime_data())
202
- self.assertEqual(30, len(data))
216
+ self.assertEqual(32, len(data))
203
217
 
204
218
  self.assertSensor('timestamp', datetime.strptime('2021-09-06 06:56:01', '%Y-%m-%d %H:%M:%S'), '', data)
205
219
  self.assertSensor('vpv1', 224.4, 'V', data)
@@ -208,18 +222,20 @@ class GW5000D_NS_Test(DtMock):
208
222
  self.assertSensor('vpv2', 291.8, 'V', data)
209
223
  self.assertSensor('ipv2', 0, 'A', data)
210
224
  self.assertSensor('ppv2', 0, 'W', data)
225
+ self.assertSensor('ppv', 0, 'W', data)
211
226
  self.assertSensor('vline1', 0, 'V', data)
212
227
  self.assertSensor('vgrid1', 240.5, 'V', data)
213
228
  self.assertSensor('igrid1', 0.0, 'A', data)
214
229
  self.assertSensor('fgrid1', 49.97, 'Hz', data)
215
230
  self.assertSensor('pgrid1', 0, 'W', data)
216
- self.assertSensor('ppv', 0, 'W', data)
231
+ self.assertSensor('total_inverter_power', 0, 'W', data)
217
232
  self.assertSensor('work_mode', 0, '', data)
218
233
  self.assertSensor('work_mode_label', 'Wait Mode', '', data)
219
234
  self.assertSensor('error_codes', 0, '', data)
220
235
  self.assertSensor('warning_code', 0, '', data)
221
236
  self.assertSensor("apparent_power", -1, "VA", data),
222
237
  self.assertSensor("reactive_power", -1, "var", data),
238
+ self.assertSensor("power_factor", -0.001, "", data),
223
239
  self.assertSensor('temperature', 1.4, 'C', data)
224
240
  self.assertSensor('e_day', 0, 'kWh', data)
225
241
  self.assertSensor('e_total', 881.7, 'kWh', data)
@@ -247,8 +263,9 @@ class GW5000_MS_Test(DtMock):
247
263
 
248
264
  def __init__(self, methodName='runTest'):
249
265
  DtMock.__init__(self, methodName)
250
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5000-MS_running_data.hex')
251
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)
252
269
 
253
270
  def test_GW6000_MS_device_info(self):
254
271
  self.loop.run_until_complete(self.read_device_info())
@@ -256,13 +273,15 @@ class GW5000_MS_Test(DtMock):
256
273
  self.assertEqual('00000MSU00000000', self.serial_number)
257
274
  self.assertEqual(12, self.dsp1_version)
258
275
  self.assertEqual(12, self.dsp2_version)
276
+ self.assertEqual(65535, self.dsp_svn_version)
259
277
  self.assertEqual(16, self.arm_version)
278
+ self.assertEqual(271, self.arm_svn_version)
260
279
  self.assertEqual('12.12.10', self.firmware)
261
280
 
262
281
  def test_GW5000_MS_runtime_data(self):
263
282
  self.loop.run_until_complete(self.read_device_info())
264
283
  data = self.loop.run_until_complete(self.read_runtime_data())
265
- self.assertEqual(33, len(data))
284
+ self.assertEqual(35, len(data))
266
285
 
267
286
  self.assertSensor('timestamp', datetime.strptime('2021-10-15 09:03:12', '%Y-%m-%d %H:%M:%S'), '', data)
268
287
  self.assertSensor('vpv1', 319.6, 'V', data)
@@ -274,18 +293,20 @@ class GW5000_MS_Test(DtMock):
274
293
  self.assertSensor('vpv3', 143.2, 'V', data)
275
294
  self.assertSensor('ipv3', 0.4, 'A', data)
276
295
  self.assertSensor('ppv3', 57, 'W', data)
296
+ self.assertSensor('ppv', 165, 'W', data)
277
297
  self.assertSensor('vline1', 0, 'V', data)
278
298
  self.assertSensor('vgrid1', 240.1, 'V', data)
279
299
  self.assertSensor('igrid1', 0.9, 'A', data)
280
300
  self.assertSensor('fgrid1', 49.98, 'Hz', data)
281
301
  self.assertSensor('pgrid1', 216, 'W', data)
282
- self.assertSensor('ppv', 295, 'W', data)
302
+ self.assertSensor('total_inverter_power', 295, 'W', data)
283
303
  self.assertSensor('work_mode', 1, '', data)
284
304
  self.assertSensor('work_mode_label', 'Normal', '', data)
285
305
  self.assertSensor('error_codes', 0, '', data)
286
306
  self.assertSensor('warning_code', 0, '', data)
287
307
  self.assertSensor("apparent_power", -1, "VA", data),
288
308
  self.assertSensor("reactive_power", -1, "var", data),
309
+ self.assertSensor("power_factor", -0.001, "", data),
289
310
  self.assertSensor('temperature', 10.7, 'C', data)
290
311
  self.assertSensor('e_day', 0.4, 'kWh', data)
291
312
  self.assertSensor('e_total', 6.8, 'kWh', data)
@@ -304,7 +325,8 @@ class GW10K_MS_30_Test(DtMock):
304
325
  def __init__(self, methodName='runTest'):
305
326
  DtMock.__init__(self, methodName)
306
327
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW10K-MS-30_device_info.hex')
307
- 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)
308
330
 
309
331
  def test_GW10K_MS_30_device_info(self):
310
332
  self.loop.run_until_complete(self.read_device_info())
@@ -312,13 +334,15 @@ class GW10K_MS_30_Test(DtMock):
312
334
  self.assertEqual('5010KMSC000W0000', self.serial_number)
313
335
  self.assertEqual(0, self.dsp1_version)
314
336
  self.assertEqual(0, self.dsp2_version)
337
+ self.assertEqual(504, self.dsp_svn_version)
315
338
  self.assertEqual(2, self.arm_version)
339
+ self.assertEqual(13, self.arm_svn_version)
316
340
  self.assertEqual('0.0.02', self.firmware)
317
341
 
318
342
  def test_GW10K_MS_30_runtime_data(self):
319
343
  self.loop.run_until_complete(self.read_device_info())
320
344
  data = self.loop.run_until_complete(self.read_runtime_data())
321
- self.assertEqual(33, len(data))
345
+ self.assertEqual(35, len(data))
322
346
 
323
347
  self.assertSensor('timestamp', datetime.strptime('2024-01-09 22:08:20', '%Y-%m-%d %H:%M:%S'), '', data)
324
348
  self.assertSensor('vpv1', 0.0, 'V', data)
@@ -330,18 +354,20 @@ class GW10K_MS_30_Test(DtMock):
330
354
  self.assertSensor('vpv3', 0.0, 'V', data)
331
355
  self.assertSensor('ipv3', 0.0, 'A', data)
332
356
  self.assertSensor('ppv3', 0, 'W', data)
357
+ self.assertSensor('ppv', 0, 'W', data)
333
358
  self.assertSensor('vline1', 0.0, 'V', data)
334
359
  self.assertSensor('vgrid1', 236.2, 'V', data)
335
360
  self.assertSensor('igrid1', 0.0, 'A', data)
336
361
  self.assertSensor('fgrid1', 50.0, 'Hz', data)
337
362
  self.assertSensor('pgrid1', 0, 'W', data)
338
- self.assertSensor('ppv', 0, 'W', data)
363
+ self.assertSensor('total_inverter_power', 0, 'W', data)
339
364
  self.assertSensor('work_mode', 0, '', data)
340
365
  self.assertSensor('work_mode_label', 'Wait Mode', '', data)
341
366
  self.assertSensor('error_codes', 0, '', data)
342
367
  self.assertSensor('warning_code', 0, '', data)
343
368
  self.assertSensor("apparent_power", 0, "VA", data),
344
369
  self.assertSensor("reactive_power", 0, "var", data),
370
+ self.assertSensor("power_factor", 0.0, "", data),
345
371
  self.assertSensor('temperature', 24.3, 'C', data)
346
372
  self.assertSensor('e_day', 71.8, 'kWh', data)
347
373
  self.assertSensor('e_total', 3433.4, 'kWh', data)
@@ -355,12 +381,69 @@ class GW10K_MS_30_Test(DtMock):
355
381
  self.assertSensor('derating_mode_label', '', '', data)
356
382
 
357
383
 
384
+ class GW10K_MS_TCP_Test(DtMock):
385
+
386
+ def __init__(self, methodName='runTest'):
387
+ DtMock.__init__(self, methodName, 502)
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)
390
+
391
+ def test_GW10K_MS_TCP_runtime_data(self):
392
+ self.loop.run_until_complete(self.read_device_info())
393
+ data = self.loop.run_until_complete(self.read_runtime_data())
394
+ self.assertEqual(42, len(data))
395
+
396
+ self.assertSensor('timestamp', datetime.strptime('2024-06-02 09:07:17', '%Y-%m-%d %H:%M:%S'), '', data)
397
+ self.assertSensor('vpv1', 400.6, 'V', data)
398
+ self.assertSensor('ipv1', 6.9, 'A', data)
399
+ self.assertSensor('ppv1', 2764, 'W', data)
400
+ self.assertSensor('vpv2', 364.0, 'V', data)
401
+ self.assertSensor('ipv2', 3.6, 'A', data)
402
+ self.assertSensor('ppv2', 1310, 'W', data)
403
+ self.assertSensor('ppv', 6143, 'W', data)
404
+ self.assertSensor('vline1', 0, 'V', data)
405
+ self.assertSensor('vline2', 0, 'V', data)
406
+ self.assertSensor('vline3', 0, 'V', data)
407
+ self.assertSensor('vgrid1', 241.1, 'V', data)
408
+ self.assertSensor('vgrid2', 0, 'V', data)
409
+ self.assertSensor('vgrid3', 0, 'V', data)
410
+ self.assertSensor('igrid1', 24.7, 'A', data)
411
+ self.assertSensor('igrid2', 0, 'A', data)
412
+ self.assertSensor('igrid3', 0, 'A', data)
413
+ self.assertSensor('fgrid1', 49.98, 'Hz', data)
414
+ self.assertSensor('fgrid2', -0.01, 'Hz', data)
415
+ self.assertSensor('fgrid3', -0.01, 'Hz', data)
416
+ self.assertSensor('pgrid1', 5955, 'W', data)
417
+ self.assertSensor('pgrid2', 0, 'W', data)
418
+ self.assertSensor('pgrid3', 0, 'W', data)
419
+ self.assertSensor('total_inverter_power', 5914, 'W', data)
420
+ self.assertSensor('work_mode', 1, '', data)
421
+ self.assertSensor('work_mode_label', 'Normal', '', data)
422
+ self.assertSensor('error_codes', 0, '', data)
423
+ self.assertSensor('warning_code', 0, '', data)
424
+ self.assertSensor('apparent_power', 5957, 'VA', data)
425
+ self.assertSensor('reactive_power', -6, 'var', data)
426
+ self.assertSensor("power_factor", 0.999, "", data),
427
+ self.assertSensor('temperature', 36.0, 'C', data)
428
+ self.assertSensor('e_day', 4.3, 'kWh', data)
429
+ self.assertSensor('e_total', 998.2, 'kWh', data)
430
+ self.assertSensor('h_total', 246, 'h', data)
431
+ self.assertSensor('safety_country', 32, '', data)
432
+ self.assertSensor('safety_country_label', '50Hz 230Vac Default', '', data)
433
+ self.assertSensor('funbit', 0, '', data)
434
+ self.assertSensor('vbus', 397.3, 'V', data)
435
+ self.assertSensor('vnbus', 0, 'V', data)
436
+ self.assertSensor('derating_mode', 0, '', data)
437
+ self.assertSensor('derating_mode_label', '', '', data)
438
+
439
+
358
440
  class GW20KAU_DT_Test(DtMock):
359
441
 
360
442
  def __init__(self, methodName='runTest'):
361
443
  DtMock.__init__(self, methodName)
362
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW20KAU-DT_running_data.hex')
363
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)
364
447
 
365
448
  def test_GW20KAU_DT_device_info(self):
366
449
  self.loop.run_until_complete(self.read_device_info())
@@ -368,13 +451,15 @@ class GW20KAU_DT_Test(DtMock):
368
451
  self.assertEqual('0000KDTA00000000', self.serial_number)
369
452
  self.assertEqual(15, self.dsp1_version)
370
453
  self.assertEqual(15, self.dsp2_version)
454
+ self.assertEqual(1099, self.dsp_svn_version)
371
455
  self.assertEqual(16, self.arm_version)
456
+ self.assertEqual(187, self.arm_svn_version)
372
457
  self.assertEqual('15.15.10', self.firmware)
373
458
 
374
459
  def test_GW20KAU_DT_runtime_data(self):
375
460
  self.loop.run_until_complete(self.read_device_info())
376
461
  data = self.loop.run_until_complete(self.read_runtime_data())
377
- self.assertEqual(40, len(data))
462
+ self.assertEqual(42, len(data))
378
463
 
379
464
  self.assertSensor('timestamp', datetime.strptime('2022-10-21 19:23:42', '%Y-%m-%d %H:%M:%S'), '', data)
380
465
  self.assertSensor('vpv1', 390.5, 'V', data)
@@ -383,6 +468,7 @@ class GW20KAU_DT_Test(DtMock):
383
468
  self.assertSensor('vpv2', 351.6, 'V', data)
384
469
  self.assertSensor('ipv2', 7.1, 'A', data)
385
470
  self.assertSensor('ppv2', 2496, 'W', data)
471
+ self.assertSensor('ppv', 5151, 'W', data)
386
472
  self.assertSensor('vline1', 388.5, 'V', data)
387
473
  self.assertSensor('vline2', 391.7, 'V', data)
388
474
  self.assertSensor('vline3', 394.5, 'V', data)
@@ -398,13 +484,14 @@ class GW20KAU_DT_Test(DtMock):
398
484
  self.assertSensor('pgrid1', 1628, 'W', data)
399
485
  self.assertSensor('pgrid2', 1655, 'W', data)
400
486
  self.assertSensor('pgrid3', 1621, 'W', data)
401
- self.assertSensor('ppv', 4957, 'W', data)
487
+ self.assertSensor('total_inverter_power', 4957, 'W', data)
402
488
  self.assertSensor('work_mode', 1, '', data)
403
489
  self.assertSensor('work_mode_label', 'Normal', '', data)
404
490
  self.assertSensor('error_codes', 0, '', data)
405
491
  self.assertSensor('warning_code', 0, '', data)
406
492
  self.assertSensor("apparent_power", 0, "VA", data),
407
493
  self.assertSensor("reactive_power", 205, "var", data),
494
+ self.assertSensor("power_factor", 0.999, "", data),
408
495
  self.assertSensor('temperature', 36.4, 'C', data)
409
496
  self.assertSensor('e_day', 19.8, 'kWh', data)
410
497
  self.assertSensor('e_total', 4304.8, 'kWh', data)
@@ -422,8 +509,9 @@ class GW17K_DT_Test(DtMock):
422
509
 
423
510
  def __init__(self, methodName='runTest'):
424
511
  DtMock.__init__(self, methodName)
425
- self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW17K-DT_running_data.hex')
426
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)
427
515
 
428
516
  def test_GW20KAU_DT_device_info(self):
429
517
  self.loop.run_until_complete(self.read_device_info())
@@ -431,13 +519,15 @@ class GW17K_DT_Test(DtMock):
431
519
  self.assertEqual('5017KDTT00BW0000', self.serial_number)
432
520
  self.assertEqual(12, self.dsp1_version)
433
521
  self.assertEqual(12, self.dsp2_version)
522
+ self.assertEqual(931, self.dsp_svn_version)
434
523
  self.assertEqual(13, self.arm_version)
524
+ self.assertEqual(130, self.arm_svn_version)
435
525
  self.assertEqual('12.12.0d', self.firmware)
436
526
 
437
527
  def test_GW20KAU_DT_runtime_data(self):
438
528
  self.loop.run_until_complete(self.read_device_info())
439
529
  data = self.loop.run_until_complete(self.read_runtime_data())
440
- self.assertEqual(40, len(data))
530
+ self.assertEqual(42, len(data))
441
531
 
442
532
  self.assertSensor('timestamp', datetime.strptime('2024-05-20 10:35:55', '%Y-%m-%d %H:%M:%S'), '', data)
443
533
  self.assertSensor('vpv1', 540.0, 'V', data)
@@ -446,6 +536,7 @@ class GW17K_DT_Test(DtMock):
446
536
  self.assertSensor('vpv2', 475.5, 'V', data)
447
537
  self.assertSensor('ipv2', 14.8, 'A', data)
448
538
  self.assertSensor('ppv2', 7037, 'W', data)
539
+ self.assertSensor('ppv', 12707, 'W', data)
449
540
  self.assertSensor('vline1', 413.0, 'V', data)
450
541
  self.assertSensor('vline2', 411.5, 'V', data)
451
542
  self.assertSensor('vline3', 409.5, 'V', data)
@@ -461,13 +552,14 @@ class GW17K_DT_Test(DtMock):
461
552
  self.assertSensor('pgrid1', 4166, 'W', data)
462
553
  self.assertSensor('pgrid2', 4170, 'W', data)
463
554
  self.assertSensor('pgrid3', 4153, 'W', data)
464
- self.assertSensor('ppv', 12470, 'W', data)
555
+ self.assertSensor('total_inverter_power', 12470, 'W', data)
465
556
  self.assertSensor('work_mode', 1, '', data)
466
557
  self.assertSensor('work_mode_label', 'Normal', '', data)
467
558
  self.assertSensor('error_codes', 0, '', data)
468
559
  self.assertSensor('warning_code', 0, '', data)
469
560
  self.assertSensor('apparent_power', 0, 'VA', data)
470
561
  self.assertSensor('reactive_power', 0, 'var', data)
562
+ self.assertSensor("power_factor", 0.0, "", data),
471
563
  self.assertSensor('temperature', 45.7, 'C', data)
472
564
  self.assertSensor('e_day', 29.3, 'kWh', data)
473
565
  self.assertSensor('e_total', 29984.4, 'kWh', data)
@@ -15,7 +15,7 @@ class EtMock(TestCase, ET):
15
15
  def __init__(self, methodName='runTest'):
16
16
  TestCase.__init__(self, methodName)
17
17
  ET.__init__(self, "localhost", 8899)
18
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
18
+ self.sensor_map = {s.id_: s for s in self.sensors()}
19
19
  self._mock_responses = {}
20
20
  self._list_of_requests = []
21
21
 
@@ -41,10 +41,11 @@ class EtMock(TestCase, ET):
41
41
  self._list_of_requests.append(command.request)
42
42
  return ProtocolResponse(bytes.fromhex("aa55f700010203040506070809"), command)
43
43
 
44
- def assertSensor(self, sensor, expected_value, expected_unit, data):
45
- self.assertEqual(expected_value, data.get(sensor))
46
- self.assertEqual(expected_unit, self.sensor_map.get(sensor))
47
- self.sensor_map.pop(sensor)
44
+ def assertSensor(self, sensor_name, expected_value, expected_unit, data):
45
+ self.assertEqual(expected_value, data.get(sensor_name))
46
+ sensor = self.sensor_map.get(sensor_name);
47
+ self.assertEqual(expected_unit, sensor.unit)
48
+ self.sensor_map.pop(sensor_name)
48
49
 
49
50
  @classmethod
50
51
  def setUpClass(cls):
@@ -81,13 +82,15 @@ class GW10K_ET_Test(EtMock):
81
82
  def test_GW10K_ET_runtime_data(self):
82
83
  # Reset sensors
83
84
  self.loop.run_until_complete(self.read_device_info())
84
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
85
+ self.sensor_map = {s.id_: s for s in self.sensors()}
85
86
 
86
87
  data = self.loop.run_until_complete(self.read_runtime_data())
87
88
  self.assertEqual(145, len(data))
88
89
 
90
+ self.assertEqual(36015, self.sensor_map.get("meter_e_total_exp").offset)
91
+
89
92
  # for sensor in self.sensors():
90
- # print(f"self.assertSensor('{sensor.id_}', {data[sensor.id_]}, '{self.sensor_map.get(sensor.id_)}', data)")
93
+ # print(f"self.assertSensor('{sensor.id_}', {data[sensor.id_]}, '{self.sensor_map.get(sensor.id_).unit}', data)")
91
94
 
92
95
  self.assertSensor('timestamp', datetime.strptime('2021-08-22 11:11:12', '%Y-%m-%d %H:%M:%S'), '', data)
93
96
  self.assertSensor('vpv1', 332.6, 'V', data)
@@ -386,7 +389,7 @@ class GW10K_ET_fw1023_Test(EtMock):
386
389
  def test_GW10K_ET_runtime_data_fw1023(self):
387
390
  # Reset sensors
388
391
  self.loop.run_until_complete(self.read_device_info())
389
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
392
+ self.sensor_map = {s.id_: s for s in self.sensors()}
390
393
 
391
394
  data = self.loop.run_until_complete(self.read_runtime_data())
392
395
  self.assertEqual(145, len(data))
@@ -596,7 +599,7 @@ class GEH10_1U_10_Test(EtMock):
596
599
  def test_GEH10_1U_10_runtime_data(self):
597
600
  # Reset sensors
598
601
  self.loop.run_until_complete(self.read_device_info())
599
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
602
+ self.sensor_map = {s.id_: s for s in self.sensors()}
600
603
 
601
604
  data = self.loop.run_until_complete(self.read_runtime_data())
602
605
  self.assertEqual(125, len(data))
@@ -760,6 +763,7 @@ class GW25K_ET_Test(EtMock):
760
763
  EtMock.__init__(self, methodName)
761
764
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW25K-ET_device_info.hex')
762
765
  self.mock_response(self._READ_RUNNING_DATA, 'GW25K-ET_running_data.hex')
766
+ self.mock_response(self._READ_METER_DATA_EXTENDED2, ILLEGAL_DATA_ADDRESS)
763
767
  self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW25K-ET_meter_data.hex')
764
768
  self.mock_response(self._READ_BATTERY_INFO, 'GW25K-ET_battery_info.hex')
765
769
  self.mock_response(self._READ_MPPT_DATA, 'GW25K-ET_mppt_data.hex')
@@ -782,11 +786,14 @@ class GW25K_ET_Test(EtMock):
782
786
  def test_GW25K_ET_runtime_data(self):
783
787
  # Reset sensors
784
788
  self.loop.run_until_complete(self.read_device_info())
785
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
786
789
 
787
790
  data = self.loop.run_until_complete(self.read_runtime_data())
788
791
  self.assertEqual(237, len(data))
789
792
 
793
+ self.sensor_map = {s.id_: s for s in self.sensors()}
794
+
795
+ # self.assertEqual(36104, self.sensor_map.get("meter_e_total_exp").offset)
796
+
790
797
  self.assertSensor('timestamp', datetime.strptime('2023-12-03 14:07:07', '%Y-%m-%d %H:%M:%S'), '', data)
791
798
  self.assertSensor('vpv1', 737.9, 'V', data)
792
799
  self.assertSensor('ipv1', 1.4, 'A', data)
@@ -1036,6 +1043,7 @@ class GW29K9_ET_Test(EtMock):
1036
1043
  EtMock.__init__(self, methodName)
1037
1044
  self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW29K9-ET_device_info.hex')
1038
1045
  self.mock_response(self._READ_RUNNING_DATA, 'GW29K9-ET_running_data.hex')
1046
+ self.mock_response(self._READ_METER_DATA_EXTENDED2, ILLEGAL_DATA_ADDRESS)
1039
1047
  self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW29K9-ET_meter_data.hex')
1040
1048
  self.mock_response(self._READ_BATTERY_INFO, 'GW29K9-ET_battery_info.hex')
1041
1049
  self.mock_response(self._READ_BATTERY2_INFO, 'GW29K9-ET_battery2_info.hex')
@@ -1059,11 +1067,12 @@ class GW29K9_ET_Test(EtMock):
1059
1067
  def test_GW29K9_ET_runtime_data(self):
1060
1068
  # Reset sensors
1061
1069
  self.loop.run_until_complete(self.read_device_info())
1062
- self.sensor_map = {s.id_: s.unit for s in self.sensors()}
1063
1070
 
1064
1071
  data = self.loop.run_until_complete(self.read_runtime_data())
1065
1072
  self.assertEqual(211, len(data))
1066
1073
 
1074
+ self.sensor_map = {s.id_: s for s in self.sensors()}
1075
+
1067
1076
  self.assertSensor('timestamp', datetime.strptime('2024-01-17 14:49:14', '%Y-%m-%d %H:%M:%S'), '', data)
1068
1077
  self.assertSensor('vpv1', 682.9, 'V', data)
1069
1078
  self.assertSensor('ipv1', 1.5, 'A', data)
@@ -1206,32 +1215,6 @@ class GW29K9_ET_Test(EtMock):
1206
1215
  self.assertSensor('meter_current1', 4.6, 'A', data)
1207
1216
  self.assertSensor('meter_current2', 6.0, 'A', data)
1208
1217
  self.assertSensor('meter_current3', 13.6, 'A', data)
1209
- self.assertSensor('battery_bms', None, '', data)
1210
- self.assertSensor('battery_index', None, '', data)
1211
- self.assertSensor('battery_status', None, '', data)
1212
- self.assertSensor('battery_temperature', None, 'C', data)
1213
- self.assertSensor('battery_charge_limit', None, 'A', data)
1214
- self.assertSensor('battery_discharge_limit', None, 'A', data)
1215
- self.assertSensor('battery_error_l', None, '', data)
1216
- self.assertSensor('battery_soc', None, '%', data)
1217
- self.assertSensor('battery_soh', None, '%', data)
1218
- self.assertSensor('battery_modules', None, '', data)
1219
- self.assertSensor('battery_warning_l', None, '', data)
1220
- self.assertSensor('battery_protocol', None, '', data)
1221
- self.assertSensor('battery_error_h', None, '', data)
1222
- self.assertSensor('battery_error', None, '', data)
1223
- self.assertSensor('battery_warning_h', None, '', data)
1224
- self.assertSensor('battery_warning', None, '', data)
1225
- self.assertSensor('battery_sw_version', None, '', data)
1226
- self.assertSensor('battery_hw_version', None, '', data)
1227
- self.assertSensor('battery_max_cell_temp_id', None, '', data)
1228
- self.assertSensor('battery_min_cell_temp_id', None, '', data)
1229
- self.assertSensor('battery_max_cell_voltage_id', None, '', data)
1230
- self.assertSensor('battery_min_cell_voltage_id', None, '', data)
1231
- self.assertSensor('battery_max_cell_temp', None, 'C', data)
1232
- self.assertSensor('battery_min_cell_temp', None, 'C', data)
1233
- self.assertSensor('battery_max_cell_voltage', None, 'V', data)
1234
- self.assertSensor('battery_min_cell_voltage', None, 'V', data)
1235
1218
  self.assertSensor('battery2_status', 0, '', data)
1236
1219
  self.assertSensor('battery2_temperature', 0.0, 'C', data)
1237
1220
  self.assertSensor('battery2_charge_limit', 0, 'A', 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)
@@ -169,6 +169,16 @@ class TestUtils(TestCase):
169
169
  data = MockResponse("ffffffff")
170
170
  self.assertIsNone(testee.read(data))
171
171
 
172
+ def test_energy8(self):
173
+ testee = Energy8("", 0, "", None)
174
+
175
+ data = MockResponse("0000000000015b41")
176
+ self.assertEqual(888.97, testee.read(data))
177
+ data = MockResponse("0000000000038E6C")
178
+ self.assertEqual(2330.68, testee.read(data))
179
+ data = MockResponse("ffffffffffffffff")
180
+ self.assertIsNone(testee.read(data))
181
+
172
182
  def test_temp(self):
173
183
  testee = Temp("", 0, "", None)
174
184
 
goodwe-0.4.6/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.4.6
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