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.
- {goodwe-0.4.6/goodwe.egg-info → goodwe-0.4.8}/PKG-INFO +1 -1
- goodwe-0.4.8/VERSION +1 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/dt.py +40 -10
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/et.py +35 -3
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/model.py +4 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/protocol.py +48 -45
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/sensor.py +19 -0
- {goodwe-0.4.6 → goodwe-0.4.8/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_dt.py +124 -32
- {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_et.py +20 -37
- {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_protocol.py +34 -34
- {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_sensor.py +10 -0
- goodwe-0.4.6/VERSION +0 -1
- {goodwe-0.4.6 → goodwe-0.4.8}/LICENSE +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/README.md +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/__init__.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/const.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/es.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/exceptions.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/inverter.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe/modbus.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/pyproject.toml +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/setup.cfg +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_es.py +0 -0
- {goodwe-0.4.6 → goodwe-0.4.8}/tests/test_modbus.py +0 -0
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
158
|
-
self.
|
|
159
|
-
self.
|
|
160
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
215
|
+
self._timer = asyncio.get_running_loop().call_later(self.timeout, self._timeout_mechanism)
|
|
182
216
|
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
self.
|
|
192
|
-
|
|
193
|
-
logger.debug("Max number of retries (%d) reached, request %s failed.", self.retries, self.command)
|
|
194
|
-
self.response_future.set_exception(MaxRetriesException)
|
|
195
|
-
self._close_transport()
|
|
196
|
-
|
|
197
|
-
def _close_transport(self) -> None:
|
|
198
|
-
if self._transport:
|
|
199
|
-
try:
|
|
200
|
-
self._transport.close()
|
|
201
|
-
except RuntimeError:
|
|
202
|
-
logger.debug("Failed to close transport.")
|
|
203
|
-
self._transport = None
|
|
204
|
-
# Cancel Future on connection close
|
|
205
|
-
if self.response_future and not self.response_future.done():
|
|
206
|
-
self.response_future.cancel()
|
|
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:
|
|
@@ -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",
|
|
16
|
-
self.sensor_map = {s.id_: s
|
|
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,
|
|
37
|
-
self.assertEqual(expected_value, data.get(
|
|
38
|
-
|
|
39
|
-
self.
|
|
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.
|
|
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(
|
|
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('
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
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('
|
|
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.
|
|
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(
|
|
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('
|
|
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(
|
|
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('
|
|
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(
|
|
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('
|
|
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
|
|
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,
|
|
45
|
-
self.assertEqual(expected_value, data.get(
|
|
46
|
-
|
|
47
|
-
self.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
self.protocol.
|
|
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,
|
|
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.
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@mock.patch('goodwe.protocol.asyncio.get_running_loop')
|
|
84
|
-
def test_retry_mechanism_max_retries(self, mock_get_event_loop):
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|