goodwe 0.3.4__tar.gz → 0.3.6__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.3.4/goodwe.egg-info → goodwe-0.3.6}/PKG-INFO +1 -1
- goodwe-0.3.6/VERSION +1 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/dt.py +4 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/es.py +5 -6
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/et.py +48 -10
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/model.py +4 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/sensor.py +21 -2
- {goodwe-0.3.4 → goodwe-0.3.6/goodwe.egg-info}/PKG-INFO +1 -1
- {goodwe-0.3.4 → goodwe-0.3.6}/tests/test_dt.py +2 -2
- {goodwe-0.3.4 → goodwe-0.3.6}/tests/test_es.py +15 -1
- {goodwe-0.3.4 → goodwe-0.3.6}/tests/test_et.py +59 -51
- {goodwe-0.3.4 → goodwe-0.3.6}/tests/test_sensor.py +10 -0
- goodwe-0.3.4/VERSION +0 -1
- {goodwe-0.3.4 → goodwe-0.3.6}/LICENSE +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/README.md +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/__init__.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/const.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/exceptions.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/inverter.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/modbus.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe/protocol.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe.egg-info/SOURCES.txt +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe.egg-info/dependency_links.txt +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/goodwe.egg-info/top_level.txt +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/pyproject.toml +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/setup.cfg +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/tests/test_modbus.py +0 -0
- {goodwe-0.3.4 → goodwe-0.3.6}/tests/test_protocol.py +0 -0
goodwe-0.3.6/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.6
|
|
@@ -110,6 +110,10 @@ class DT(Inverter):
|
|
|
110
110
|
Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV),
|
|
111
111
|
Integer("grid_export", 40327, "Grid Export Enabled", "", Kind.GRID),
|
|
112
112
|
Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID),
|
|
113
|
+
Integer("start", 40330, "Start / Power On", "", Kind.GRID),
|
|
114
|
+
Integer("stop", 40331, "Stop / Power Off", "", Kind.GRID),
|
|
115
|
+
Integer("restart", 40332, "Restart", "", Kind.GRID),
|
|
116
|
+
Integer("grid_export_hw", 40345, "Grid Export Enabled (HW)", "", Kind.GRID),
|
|
113
117
|
)
|
|
114
118
|
|
|
115
119
|
# Settings for single phase inverters
|
|
@@ -178,11 +178,11 @@ class ES(Inverter):
|
|
|
178
178
|
def _supports_eco_mode_v2(self) -> bool:
|
|
179
179
|
if self.arm_version < 14:
|
|
180
180
|
return False
|
|
181
|
-
if "EMU" in self.serial_number:
|
|
181
|
+
if "EMU" in self.serial_number or "EMJ" in self.serial_number:
|
|
182
182
|
return self.dsp1_version >= 11
|
|
183
|
-
if "ESU" in self.serial_number:
|
|
183
|
+
if "ESU" in self.serial_number or "ESA" in self.serial_number:
|
|
184
184
|
return self.dsp1_version >= 22
|
|
185
|
-
if "BPS" in self.serial_number:
|
|
185
|
+
if "BPS" in self.serial_number or "BPU" in self.serial_number:
|
|
186
186
|
return self.dsp1_version >= 10
|
|
187
187
|
return False
|
|
188
188
|
|
|
@@ -192,7 +192,7 @@ class ES(Inverter):
|
|
|
192
192
|
self.firmware = self._decode(response[0:5]).rstrip()
|
|
193
193
|
self.model_name = self._decode(response[5:15]).rstrip()
|
|
194
194
|
self.serial_number = self._decode(response[31:47])
|
|
195
|
-
self.
|
|
195
|
+
self.arm_firmware = self._decode(response[51:63]) # AKA software_version
|
|
196
196
|
try:
|
|
197
197
|
if len(self.firmware) >= 2:
|
|
198
198
|
self.dsp1_version = int(self.firmware[0:2])
|
|
@@ -250,10 +250,9 @@ class ES(Inverter):
|
|
|
250
250
|
# modbus can address/store only 16 bit values, read the other 8 bytes
|
|
251
251
|
if self._is_modbus_setting(setting):
|
|
252
252
|
response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
|
|
253
|
-
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
254
253
|
else:
|
|
255
254
|
response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
|
|
256
|
-
|
|
255
|
+
raw_value = setting.encode_value(value, response.response_data()[0:2])
|
|
257
256
|
else:
|
|
258
257
|
raw_value = setting.encode_value(value)
|
|
259
258
|
if len(raw_value) <= 2:
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Tuple
|
|
5
5
|
|
|
6
|
-
from .exceptions import RequestRejectedException
|
|
6
|
+
from .exceptions import RequestFailedException, RequestRejectedException
|
|
7
7
|
from .inverter import Inverter
|
|
8
8
|
from .inverter import OperationMode
|
|
9
9
|
from .inverter import SensorKind as Kind
|
|
@@ -252,7 +252,8 @@ class ET(Inverter):
|
|
|
252
252
|
Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
|
|
253
253
|
Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
|
|
254
254
|
Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
|
|
255
|
-
|
|
255
|
+
|
|
256
|
+
# Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on
|
|
256
257
|
Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
|
|
257
258
|
Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
|
|
258
259
|
Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
|
|
@@ -263,6 +264,15 @@ class ET(Inverter):
|
|
|
263
264
|
Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
|
|
264
265
|
Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
|
|
265
266
|
Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID),
|
|
267
|
+
|
|
268
|
+
Energy8("meter_e_total_exp1", 36092, "Meter Total Energy (export) L1", Kind.GRID),
|
|
269
|
+
Energy8("meter_e_total_exp2", 36096, "Meter Total Energy (export) L2", Kind.GRID),
|
|
270
|
+
Energy8("meter_e_total_exp3", 36100, "Meter Total Energy (export) L3", Kind.GRID),
|
|
271
|
+
Energy8("meter_e_total_exp", 36104, "Meter Total Energy (export)", Kind.GRID),
|
|
272
|
+
Energy8("meter_e_total_imp1", 36108, "Meter Total Energy (import) L1", Kind.GRID),
|
|
273
|
+
Energy8("meter_e_total_imp2", 36112, "Meter Total Energy (import) L2", Kind.GRID),
|
|
274
|
+
Energy8("meter_e_total_imp3", 36116, "Meter Total Energy (import) L3", Kind.GRID),
|
|
275
|
+
Energy8("meter_e_total_imp", 36120, "Meter Total Energy (import)", Kind.GRID),
|
|
266
276
|
)
|
|
267
277
|
|
|
268
278
|
# Inverter's MPPT data
|
|
@@ -414,6 +424,7 @@ class ET(Inverter):
|
|
|
414
424
|
self._READ_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x891c, 0x007d)
|
|
415
425
|
self._READ_METER_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x2d)
|
|
416
426
|
self._READ_METER_DATA_EXTENDED: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x3a)
|
|
427
|
+
self._READ_METER_DATA_EXTENDED2: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x7d)
|
|
417
428
|
self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018)
|
|
418
429
|
self._READ_BATTERY2_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9858, 0x0016)
|
|
419
430
|
self._READ_MPPT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89e5, 0x3d)
|
|
@@ -422,6 +433,7 @@ class ET(Inverter):
|
|
|
422
433
|
self._has_battery: bool = True
|
|
423
434
|
self._has_battery2: bool = False
|
|
424
435
|
self._has_meter_extended: bool = False
|
|
436
|
+
self._has_meter_extended2: bool = False
|
|
425
437
|
self._has_mppt: bool = False
|
|
426
438
|
self._sensors = self.__all_sensors
|
|
427
439
|
self._sensors_battery = self.__all_sensors_battery
|
|
@@ -440,6 +452,11 @@ class ET(Inverter):
|
|
|
440
452
|
"""Filter to exclude extended meter sensors"""
|
|
441
453
|
return s.offset < 36045
|
|
442
454
|
|
|
455
|
+
@staticmethod
|
|
456
|
+
def _not_extended_meter2(s: Sensor) -> bool:
|
|
457
|
+
"""Filter to exclude extended meter sensors"""
|
|
458
|
+
return s.offset < 36058
|
|
459
|
+
|
|
443
460
|
async def read_device_info(self):
|
|
444
461
|
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
|
|
445
462
|
response = response.response_data()
|
|
@@ -470,9 +487,10 @@ class ET(Inverter):
|
|
|
470
487
|
if is_2_battery(self) or self.rated_power >= 25000:
|
|
471
488
|
self._has_battery2 = True
|
|
472
489
|
|
|
473
|
-
if self.rated_power >= 15000:
|
|
490
|
+
if is_745_platform(self) or self.rated_power >= 15000:
|
|
474
491
|
self._has_mppt = True
|
|
475
492
|
self._has_meter_extended = True
|
|
493
|
+
self._has_meter_extended2 = True
|
|
476
494
|
else:
|
|
477
495
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
478
496
|
|
|
@@ -482,8 +500,11 @@ class ET(Inverter):
|
|
|
482
500
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
|
|
483
501
|
except RequestRejectedException as ex:
|
|
484
502
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
485
|
-
logger.debug("
|
|
503
|
+
logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
|
|
486
504
|
self._has_eco_mode_v2 = False
|
|
505
|
+
except RequestFailedException as ex:
|
|
506
|
+
logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
|
|
507
|
+
self._has_eco_mode_v2 = False
|
|
487
508
|
|
|
488
509
|
# Check and add Peak Shaving settings added in (ETU fw 22)
|
|
489
510
|
try:
|
|
@@ -491,8 +512,11 @@ class ET(Inverter):
|
|
|
491
512
|
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
|
|
492
513
|
except RequestRejectedException as ex:
|
|
493
514
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
494
|
-
logger.debug("
|
|
515
|
+
logger.debug("PeakShaving setting not supported, disabling it.")
|
|
495
516
|
self._has_peak_shaving = False
|
|
517
|
+
except RequestFailedException as ex:
|
|
518
|
+
logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
|
|
519
|
+
self._has_peak_shaving = False
|
|
496
520
|
|
|
497
521
|
async def read_runtime_data(self) -> Dict[str, Any]:
|
|
498
522
|
response = await self._read_from_socket(self._READ_RUNNING_DATA)
|
|
@@ -505,7 +529,7 @@ class ET(Inverter):
|
|
|
505
529
|
data.update(self._map_response(response, self._sensors_battery))
|
|
506
530
|
except RequestRejectedException as ex:
|
|
507
531
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
508
|
-
logger.warning("
|
|
532
|
+
logger.warning("Battery values not supported, disabling further attempts.")
|
|
509
533
|
self._has_battery = False
|
|
510
534
|
else:
|
|
511
535
|
raise ex
|
|
@@ -516,18 +540,32 @@ class ET(Inverter):
|
|
|
516
540
|
self._map_response(response, self._sensors_battery2))
|
|
517
541
|
except RequestRejectedException as ex:
|
|
518
542
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
519
|
-
logger.warning("
|
|
543
|
+
logger.warning("Battery 2 values not supported, disabling further attempts.")
|
|
520
544
|
self._has_battery2 = False
|
|
521
545
|
else:
|
|
522
546
|
raise ex
|
|
523
547
|
|
|
524
|
-
if self.
|
|
548
|
+
if self._has_meter_extended2:
|
|
549
|
+
try:
|
|
550
|
+
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2)
|
|
551
|
+
data.update(self._map_response(response, self._sensors_meter))
|
|
552
|
+
except RequestRejectedException as ex:
|
|
553
|
+
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
554
|
+
logger.info("Extended meter values not supported, disabling further attempts.")
|
|
555
|
+
self._has_meter_extended2 = False
|
|
556
|
+
self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter))
|
|
557
|
+
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
558
|
+
data.update(
|
|
559
|
+
self._map_response(response, self._sensors_meter))
|
|
560
|
+
else:
|
|
561
|
+
raise ex
|
|
562
|
+
elif self._has_meter_extended:
|
|
525
563
|
try:
|
|
526
564
|
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
|
|
527
565
|
data.update(self._map_response(response, self._sensors_meter))
|
|
528
566
|
except RequestRejectedException as ex:
|
|
529
567
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
530
|
-
logger.warning("
|
|
568
|
+
logger.warning("Extended meter values not supported, disabling further attempts.")
|
|
531
569
|
self._has_meter_extended = False
|
|
532
570
|
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
|
|
533
571
|
response = await self._read_from_socket(self._READ_METER_DATA)
|
|
@@ -545,7 +583,7 @@ class ET(Inverter):
|
|
|
545
583
|
data.update(self._map_response(response, self._sensors_mppt))
|
|
546
584
|
except RequestRejectedException as ex:
|
|
547
585
|
if ex.message == 'ILLEGAL DATA ADDRESS':
|
|
548
|
-
logger.warning("
|
|
586
|
+
logger.warning("MPPT values not supported, disabling further attempts.")
|
|
549
587
|
self._has_mppt = False
|
|
550
588
|
else:
|
|
551
589
|
raise ex
|
|
@@ -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)
|
|
@@ -183,7 +183,7 @@ class Energy(Sensor):
|
|
|
183
183
|
|
|
184
184
|
def read_value(self, data: ProtocolResponse):
|
|
185
185
|
value = read_bytes2(data)
|
|
186
|
-
return float(value) / 10 if value else None
|
|
186
|
+
return float(value) / 10 if value is not None else None
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
class Energy4(Sensor):
|
|
@@ -194,7 +194,18 @@ class Energy4(Sensor):
|
|
|
194
194
|
|
|
195
195
|
def read_value(self, data: ProtocolResponse):
|
|
196
196
|
value = read_bytes4(data)
|
|
197
|
-
return float(value) / 10 if value else None
|
|
197
|
+
return float(value) / 10 if value is not None else None
|
|
198
|
+
|
|
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
|
|
198
209
|
|
|
199
210
|
|
|
200
211
|
class Apparent(Sensor):
|
|
@@ -816,6 +827,14 @@ def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
|
|
|
816
827
|
return int.from_bytes(buffer.read(4), byteorder="big", signed=True)
|
|
817
828
|
|
|
818
829
|
|
|
830
|
+
def read_bytes8(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int:
|
|
831
|
+
"""Retrieve 8 byte (unsigned int) value from buffer"""
|
|
832
|
+
if offset is not None:
|
|
833
|
+
buffer.seek(offset)
|
|
834
|
+
value = int.from_bytes(buffer.read(8), byteorder="big", signed=False)
|
|
835
|
+
return undef if value == 0xffffffffffffffff else value
|
|
836
|
+
|
|
837
|
+
|
|
819
838
|
def read_decimal2(buffer: ProtocolResponse, scale: int, offset: int = None) -> float:
|
|
820
839
|
"""Retrieve 2 byte (signed float) value from buffer"""
|
|
821
840
|
if offset is not None:
|
|
@@ -101,7 +101,7 @@ class GW6000_DT_Test(DtMock):
|
|
|
101
101
|
self.assertFalse(self.sensor_map, f"Some sensors were not tested {self.sensor_map}")
|
|
102
102
|
|
|
103
103
|
def test_GW6000_DT_setting(self):
|
|
104
|
-
self.assertEqual(
|
|
104
|
+
self.assertEqual(8, len(self.settings()))
|
|
105
105
|
settings = {s.id_: s for s in self.settings()}
|
|
106
106
|
self.assertEqual('Timestamp', type(settings.get("time")).__name__)
|
|
107
107
|
self.assertEqual('Integer', type(settings.get("grid_export")).__name__)
|
|
@@ -221,7 +221,7 @@ class GW5000D_NS_Test(DtMock):
|
|
|
221
221
|
self.assertSensor("apparent_power", -1, "VA", data),
|
|
222
222
|
self.assertSensor("reactive_power", -1, "var", data),
|
|
223
223
|
self.assertSensor('temperature', 1.4, 'C', data)
|
|
224
|
-
self.assertSensor('e_day',
|
|
224
|
+
self.assertSensor('e_day', 0, 'kWh', data)
|
|
225
225
|
self.assertSensor('e_total', 881.7, 'kWh', data)
|
|
226
226
|
self.assertSensor('h_total', 955, 'h', data)
|
|
227
227
|
self.assertSensor('safety_country', 73, '', data)
|
|
@@ -7,7 +7,7 @@ from goodwe import DISCOVERY_COMMAND
|
|
|
7
7
|
from goodwe.es import ES
|
|
8
8
|
from goodwe.exceptions import RequestFailedException
|
|
9
9
|
from goodwe.inverter import OperationMode
|
|
10
|
-
from goodwe.protocol import ProtocolCommand, ProtocolResponse
|
|
10
|
+
from goodwe.protocol import Aa55ReadCommand, ProtocolCommand, ProtocolResponse
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EsMock(TestCase, ES):
|
|
@@ -26,6 +26,8 @@ class EsMock(TestCase, ES):
|
|
|
26
26
|
root_dir = os.path.dirname(os.path.abspath(__file__))
|
|
27
27
|
filename = self._mock_responses.get(command)
|
|
28
28
|
if filename is not None:
|
|
29
|
+
if filename.startswith('aa55'):
|
|
30
|
+
return ProtocolResponse(bytes.fromhex(filename), command)
|
|
29
31
|
with open(root_dir + '/sample/es/' + filename, 'r') as f:
|
|
30
32
|
response = bytes.fromhex(f.read())
|
|
31
33
|
if not command.validator(response):
|
|
@@ -52,6 +54,8 @@ class GW5048D_ES_Test(EsMock):
|
|
|
52
54
|
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW5048D-ES_device_info.hex')
|
|
53
55
|
self.mock_response(self._READ_DEVICE_RUNNING_DATA, 'GW5048D-ES_running_data.hex')
|
|
54
56
|
self.mock_response(self._READ_DEVICE_SETTINGS_DATA, 'GW5048D-ES_settings_data.hex')
|
|
57
|
+
self.mock_response(Aa55ReadCommand(1793, 1), 'aa557fc0019a08000000000000007f0360')
|
|
58
|
+
self.mock_response(Aa55ReadCommand(1800, 1), 'aa557fc0019a02007f035a')
|
|
55
59
|
|
|
56
60
|
def test_GW5048D_ES_device_info(self):
|
|
57
61
|
self.loop.run_until_complete(self.read_device_info())
|
|
@@ -163,6 +167,16 @@ class GW5048D_ES_Test(EsMock):
|
|
|
163
167
|
data = self.loop.run_until_complete(self.read_setting('grid_export_limit'))
|
|
164
168
|
self.assertEqual(10000, data)
|
|
165
169
|
|
|
170
|
+
data = self.loop.run_until_complete(self.read_setting('eco_mode_1'))
|
|
171
|
+
self.assertEqual(
|
|
172
|
+
"EcoModeV1(id_='eco_mode_1', offset=1793, name='Eco Mode Group 1', size_=8, unit='', kind=<SensorKind.BAT: 4>)",
|
|
173
|
+
repr(data))
|
|
174
|
+
data = self.loop.run_until_complete(self.read_setting('eco_mode_2_switch'))
|
|
175
|
+
self.assertEqual(0, data)
|
|
176
|
+
|
|
177
|
+
def test_write_setting(self):
|
|
178
|
+
self.loop.run_until_complete(self.write_setting('eco_mode_2_switch', 0))
|
|
179
|
+
|
|
166
180
|
|
|
167
181
|
class GW5048_EM_Test(EsMock):
|
|
168
182
|
|
|
@@ -14,7 +14,7 @@ class EtMock(TestCase, ET):
|
|
|
14
14
|
def __init__(self, methodName='runTest'):
|
|
15
15
|
TestCase.__init__(self, methodName)
|
|
16
16
|
ET.__init__(self, "localhost")
|
|
17
|
-
self.sensor_map = {s.id_: s
|
|
17
|
+
self.sensor_map = {s.id_: s for s in self.sensors()}
|
|
18
18
|
self._mock_responses = {}
|
|
19
19
|
self._list_of_requests = []
|
|
20
20
|
|
|
@@ -28,6 +28,8 @@ class EtMock(TestCase, ET):
|
|
|
28
28
|
if filename is not None:
|
|
29
29
|
if 'ILLEGAL DATA ADDRESS' == filename:
|
|
30
30
|
raise RequestRejectedException('ILLEGAL DATA ADDRESS')
|
|
31
|
+
if 'NO RESPONSE' == filename:
|
|
32
|
+
raise RequestFailedException()
|
|
31
33
|
with open(root_dir + '/sample/et/' + filename, 'r') as f:
|
|
32
34
|
response = bytes.fromhex(f.read())
|
|
33
35
|
if not command.validator(response):
|
|
@@ -38,10 +40,11 @@ class EtMock(TestCase, ET):
|
|
|
38
40
|
self._list_of_requests.append(command.request)
|
|
39
41
|
return ProtocolResponse(bytes.fromhex("aa55f700010203040506070809"), command)
|
|
40
42
|
|
|
41
|
-
def assertSensor(self,
|
|
42
|
-
self.assertEqual(expected_value, data.get(
|
|
43
|
-
|
|
44
|
-
self.
|
|
43
|
+
def assertSensor(self, sensor_name, expected_value, expected_unit, data):
|
|
44
|
+
self.assertEqual(expected_value, data.get(sensor_name))
|
|
45
|
+
sensor = self.sensor_map.get(sensor_name)
|
|
46
|
+
self.assertEqual(expected_unit, sensor.unit)
|
|
47
|
+
self.sensor_map.pop(sensor_name)
|
|
45
48
|
|
|
46
49
|
@classmethod
|
|
47
50
|
def setUpClass(cls):
|
|
@@ -78,13 +81,15 @@ class GW10K_ET_Test(EtMock):
|
|
|
78
81
|
def test_GW10K_ET_runtime_data(self):
|
|
79
82
|
# Reset sensors
|
|
80
83
|
self.loop.run_until_complete(self.read_device_info())
|
|
81
|
-
self.sensor_map = {s.id_: s
|
|
84
|
+
self.sensor_map = {s.id_: s for s in self.sensors()}
|
|
82
85
|
|
|
83
86
|
data = self.loop.run_until_complete(self.read_runtime_data())
|
|
84
87
|
self.assertEqual(145, len(data))
|
|
85
88
|
|
|
89
|
+
self.assertEqual(36015, self.sensor_map.get("meter_e_total_exp").offset)
|
|
90
|
+
|
|
86
91
|
# for sensor in self.sensors():
|
|
87
|
-
# print(f"self.assertSensor('{sensor.id_}', {data[sensor.id_]}, '{self.sensor_map.get(sensor.id_)}', data)")
|
|
92
|
+
# print(f"self.assertSensor('{sensor.id_}', {data[sensor.id_]}, '{self.sensor_map.get(sensor.id_).unit}', data)")
|
|
88
93
|
|
|
89
94
|
self.assertSensor('timestamp', datetime.strptime('2021-08-22 11:11:12', '%Y-%m-%d %H:%M:%S'), '', data)
|
|
90
95
|
self.assertSensor('vpv1', 332.6, 'V', data)
|
|
@@ -164,7 +169,7 @@ class GW10K_ET_Test(EtMock):
|
|
|
164
169
|
self.assertSensor('h_total', 9246, 'h', data)
|
|
165
170
|
self.assertSensor("e_day_exp", 9.8, 'kWh', data)
|
|
166
171
|
self.assertSensor("e_total_imp", 58.0, 'kWh', data)
|
|
167
|
-
self.assertSensor("e_day_imp",
|
|
172
|
+
self.assertSensor("e_day_imp", 0, 'kWh', data)
|
|
168
173
|
self.assertSensor("e_load_total", 8820.2, 'kWh', data)
|
|
169
174
|
self.assertSensor("e_load_day", 11.6, 'kWh', data)
|
|
170
175
|
self.assertSensor("e_bat_charge_total", 2758.1, 'kWh', data)
|
|
@@ -457,14 +462,14 @@ class GW6000_EH_Test(EtMock):
|
|
|
457
462
|
self.assertSensor("e_total_exp", 58.6, 'kWh', data)
|
|
458
463
|
self.assertSensor('h_total', 33, 'h', data)
|
|
459
464
|
self.assertSensor("e_day_exp", 21.6, 'kWh', data)
|
|
460
|
-
self.assertSensor("e_total_imp",
|
|
461
|
-
self.assertSensor("e_day_imp",
|
|
465
|
+
self.assertSensor("e_total_imp", 0, 'kWh', data)
|
|
466
|
+
self.assertSensor("e_day_imp", 0, 'kWh', data)
|
|
462
467
|
self.assertSensor("e_load_total", 70.1, 'kWh', data)
|
|
463
468
|
self.assertSensor("e_load_day", 27.1, 'kWh', data)
|
|
464
|
-
self.assertSensor("e_bat_charge_total",
|
|
465
|
-
self.assertSensor("e_bat_charge_day",
|
|
466
|
-
self.assertSensor("e_bat_discharge_total",
|
|
467
|
-
self.assertSensor("e_bat_discharge_day",
|
|
469
|
+
self.assertSensor("e_bat_charge_total", 0, 'kWh', data)
|
|
470
|
+
self.assertSensor("e_bat_charge_day", 0, 'kWh', data)
|
|
471
|
+
self.assertSensor("e_bat_discharge_total", 0, 'kWh', data)
|
|
472
|
+
self.assertSensor("e_bat_discharge_day", 0, 'kWh', data)
|
|
468
473
|
self.assertSensor('diagnose_result', 117983303, '', data)
|
|
469
474
|
self.assertSensor('diagnose_result_label',
|
|
470
475
|
'Battery voltage low, Battery SOC low, Battery SOC in back, Discharge Driver On, Self-use load light, Battery Disconnected, Self-use off, Export power limit set, PF value set, Real power limit set',
|
|
@@ -486,7 +491,7 @@ class GEH10_1U_10_Test(EtMock):
|
|
|
486
491
|
def test_GEH10_1U_10_runtime_data(self):
|
|
487
492
|
# Reset sensors
|
|
488
493
|
self.loop.run_until_complete(self.read_device_info())
|
|
489
|
-
self.sensor_map = {s.id_: s
|
|
494
|
+
self.sensor_map = {s.id_: s for s in self.sensors()}
|
|
490
495
|
|
|
491
496
|
data = self.loop.run_until_complete(self.read_runtime_data())
|
|
492
497
|
self.assertEqual(125, len(data))
|
|
@@ -558,8 +563,8 @@ class GEH10_1U_10_Test(EtMock):
|
|
|
558
563
|
self.assertSensor('e_total_exp', 10273.3, 'kWh', data)
|
|
559
564
|
self.assertSensor('h_total', 3256, 'h', data)
|
|
560
565
|
self.assertSensor('e_day_exp', 16.6, 'kWh', data)
|
|
561
|
-
self.assertSensor('e_total_imp',
|
|
562
|
-
self.assertSensor('e_day_imp',
|
|
566
|
+
self.assertSensor('e_total_imp', 0, 'kWh', data)
|
|
567
|
+
self.assertSensor('e_day_imp', 0, 'kWh', data)
|
|
563
568
|
self.assertSensor('e_load_total', 4393.9, 'kWh', data)
|
|
564
569
|
self.assertSensor('e_load_day', 10.7, 'kWh', data)
|
|
565
570
|
self.assertSensor('e_bat_charge_total', 141.9, 'kWh', data)
|
|
@@ -650,6 +655,7 @@ class GW25K_ET_Test(EtMock):
|
|
|
650
655
|
EtMock.__init__(self, methodName)
|
|
651
656
|
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW25K-ET_device_info.hex')
|
|
652
657
|
self.mock_response(self._READ_RUNNING_DATA, 'GW25K-ET_running_data.hex')
|
|
658
|
+
self.mock_response(self._READ_METER_DATA_EXTENDED2, 'ILLEGAL DATA ADDRESS')
|
|
653
659
|
self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW25K-ET_meter_data.hex')
|
|
654
660
|
self.mock_response(self._READ_BATTERY_INFO, 'GW25K-ET_battery_info.hex')
|
|
655
661
|
self.mock_response(self._READ_MPPT_DATA, 'GW25K-ET_mppt_data.hex')
|
|
@@ -672,11 +678,14 @@ class GW25K_ET_Test(EtMock):
|
|
|
672
678
|
def test_GW25K_ET_runtime_data(self):
|
|
673
679
|
# Reset sensors
|
|
674
680
|
self.loop.run_until_complete(self.read_device_info())
|
|
675
|
-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
|
|
676
681
|
|
|
677
682
|
data = self.loop.run_until_complete(self.read_runtime_data())
|
|
678
683
|
self.assertEqual(237, len(data))
|
|
679
684
|
|
|
685
|
+
self.sensor_map = {s.id_: s for s in self.sensors()}
|
|
686
|
+
|
|
687
|
+
# self.assertEqual(36104, self.sensor_map.get("meter_e_total_exp").offset)
|
|
688
|
+
|
|
680
689
|
self.assertSensor('timestamp', datetime.strptime('2023-12-03 14:07:07', '%Y-%m-%d %H:%M:%S'), '', data)
|
|
681
690
|
self.assertSensor('vpv1', 737.9, 'V', data)
|
|
682
691
|
self.assertSensor('ipv1', 1.4, 'A', data)
|
|
@@ -771,7 +780,7 @@ class GW25K_ET_Test(EtMock):
|
|
|
771
780
|
self.assertSensor('e_bat_charge_total', 91.3, 'kWh', data)
|
|
772
781
|
self.assertSensor('e_bat_charge_day', 11.0, 'kWh', data)
|
|
773
782
|
self.assertSensor('e_bat_discharge_total', 69.6, 'kWh', data)
|
|
774
|
-
self.assertSensor('e_bat_discharge_day',
|
|
783
|
+
self.assertSensor('e_bat_discharge_day', 0, 'kWh', data)
|
|
775
784
|
self.assertSensor('diagnose_result', 33816960, '', data)
|
|
776
785
|
self.assertSensor('diagnose_result_label',
|
|
777
786
|
'BMS: Discharge current low, APP: Discharge current too low, BMS: Charge disabled, PF value set',
|
|
@@ -926,6 +935,7 @@ class GW29K9_ET_Test(EtMock):
|
|
|
926
935
|
EtMock.__init__(self, methodName)
|
|
927
936
|
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW29K9-ET_device_info.hex')
|
|
928
937
|
self.mock_response(self._READ_RUNNING_DATA, 'GW29K9-ET_running_data.hex')
|
|
938
|
+
self.mock_response(self._READ_METER_DATA_EXTENDED2, 'ILLEGAL DATA ADDRESS')
|
|
929
939
|
self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW29K9-ET_meter_data.hex')
|
|
930
940
|
self.mock_response(self._READ_BATTERY_INFO, 'GW29K9-ET_battery_info.hex')
|
|
931
941
|
self.mock_response(self._READ_BATTERY2_INFO, 'GW29K9-ET_battery2_info.hex')
|
|
@@ -949,11 +959,12 @@ class GW29K9_ET_Test(EtMock):
|
|
|
949
959
|
def test_GW29K9_ET_runtime_data(self):
|
|
950
960
|
# Reset sensors
|
|
951
961
|
self.loop.run_until_complete(self.read_device_info())
|
|
952
|
-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
|
|
953
962
|
|
|
954
963
|
data = self.loop.run_until_complete(self.read_runtime_data())
|
|
955
964
|
self.assertEqual(211, len(data))
|
|
956
965
|
|
|
966
|
+
self.sensor_map = {s.id_: s for s in self.sensors()}
|
|
967
|
+
|
|
957
968
|
self.assertSensor('timestamp', datetime.strptime('2024-01-17 14:49:14', '%Y-%m-%d %H:%M:%S'), '', data)
|
|
958
969
|
self.assertSensor('vpv1', 682.9, 'V', data)
|
|
959
970
|
self.assertSensor('ipv1', 1.5, 'A', data)
|
|
@@ -1042,13 +1053,13 @@ class GW29K9_ET_Test(EtMock):
|
|
|
1042
1053
|
self.assertSensor('h_total', 1175, 'h', data)
|
|
1043
1054
|
self.assertSensor('e_day_exp', 1.2, 'kWh', data)
|
|
1044
1055
|
self.assertSensor('e_total_imp', 8.7, 'kWh', data)
|
|
1045
|
-
self.assertSensor('e_day_imp',
|
|
1056
|
+
self.assertSensor('e_day_imp', 0, 'kWh', data)
|
|
1046
1057
|
self.assertSensor('e_load_total', 10742.2, 'kWh', data)
|
|
1047
1058
|
self.assertSensor('e_load_day', 43.8, 'kWh', data)
|
|
1048
|
-
self.assertSensor('e_bat_charge_total',
|
|
1049
|
-
self.assertSensor('e_bat_charge_day',
|
|
1050
|
-
self.assertSensor('e_bat_discharge_total',
|
|
1051
|
-
self.assertSensor('e_bat_discharge_day',
|
|
1059
|
+
self.assertSensor('e_bat_charge_total', 0, 'kWh', data)
|
|
1060
|
+
self.assertSensor('e_bat_charge_day', 0, 'kWh', data)
|
|
1061
|
+
self.assertSensor('e_bat_discharge_total', 0, 'kWh', data)
|
|
1062
|
+
self.assertSensor('e_bat_discharge_day', 0, 'kWh', data)
|
|
1052
1063
|
self.assertSensor('diagnose_result', 33816782, '', data)
|
|
1053
1064
|
self.assertSensor('diagnose_result_label',
|
|
1054
1065
|
'Battery SOC low, Battery SOC in back, BMS: Discharge disabled, '
|
|
@@ -1096,32 +1107,6 @@ class GW29K9_ET_Test(EtMock):
|
|
|
1096
1107
|
self.assertSensor('meter_current1', 4.6, 'A', data)
|
|
1097
1108
|
self.assertSensor('meter_current2', 6.0, 'A', data)
|
|
1098
1109
|
self.assertSensor('meter_current3', 13.6, 'A', data)
|
|
1099
|
-
self.assertSensor('battery_bms', None, '', data)
|
|
1100
|
-
self.assertSensor('battery_index', None, '', data)
|
|
1101
|
-
self.assertSensor('battery_status', None, '', data)
|
|
1102
|
-
self.assertSensor('battery_temperature', None, 'C', data)
|
|
1103
|
-
self.assertSensor('battery_charge_limit', None, 'A', data)
|
|
1104
|
-
self.assertSensor('battery_discharge_limit', None, 'A', data)
|
|
1105
|
-
self.assertSensor('battery_error_l', None, '', data)
|
|
1106
|
-
self.assertSensor('battery_soc', None, '%', data)
|
|
1107
|
-
self.assertSensor('battery_soh', None, '%', data)
|
|
1108
|
-
self.assertSensor('battery_modules', None, '', data)
|
|
1109
|
-
self.assertSensor('battery_warning_l', None, '', data)
|
|
1110
|
-
self.assertSensor('battery_protocol', None, '', data)
|
|
1111
|
-
self.assertSensor('battery_error_h', None, '', data)
|
|
1112
|
-
self.assertSensor('battery_error', None, '', data)
|
|
1113
|
-
self.assertSensor('battery_warning_h', None, '', data)
|
|
1114
|
-
self.assertSensor('battery_warning', None, '', data)
|
|
1115
|
-
self.assertSensor('battery_sw_version', None, '', data)
|
|
1116
|
-
self.assertSensor('battery_hw_version', None, '', data)
|
|
1117
|
-
self.assertSensor('battery_max_cell_temp_id', None, '', data)
|
|
1118
|
-
self.assertSensor('battery_min_cell_temp_id', None, '', data)
|
|
1119
|
-
self.assertSensor('battery_max_cell_voltage_id', None, '', data)
|
|
1120
|
-
self.assertSensor('battery_min_cell_voltage_id', None, '', data)
|
|
1121
|
-
self.assertSensor('battery_max_cell_temp', None, 'C', data)
|
|
1122
|
-
self.assertSensor('battery_min_cell_temp', None, 'C', data)
|
|
1123
|
-
self.assertSensor('battery_max_cell_voltage', None, 'V', data)
|
|
1124
|
-
self.assertSensor('battery_min_cell_voltage', None, 'V', data)
|
|
1125
1110
|
self.assertSensor('battery2_status', 0, '', data)
|
|
1126
1111
|
self.assertSensor('battery2_temperature', 0.0, 'C', data)
|
|
1127
1112
|
self.assertSensor('battery2_charge_limit', 0, 'A', data)
|
|
@@ -1196,3 +1181,26 @@ class GW29K9_ET_Test(EtMock):
|
|
|
1196
1181
|
self.assertSensor('apparent_power3', 0, 'VA', data)
|
|
1197
1182
|
|
|
1198
1183
|
self.assertFalse(self.sensor_map, f"Some sensors were not tested {self.sensor_map}")
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
class GW5K_BT_Test(EtMock):
|
|
1187
|
+
|
|
1188
|
+
def __init__(self, methodName='runTest'):
|
|
1189
|
+
EtMock.__init__(self, methodName)
|
|
1190
|
+
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW5K-BT_device_info.hex')
|
|
1191
|
+
self.mock_response(ModbusReadCommand(self.comm_addr, 47547, 6), 'NO RESPONSE')
|
|
1192
|
+
|
|
1193
|
+
def test_GW5K_BT_device_info(self):
|
|
1194
|
+
self.loop.run_until_complete(self.read_device_info())
|
|
1195
|
+
self.assertEqual('GW5K-BT', self.model_name)
|
|
1196
|
+
self.assertEqual('95000BTU203W0000', self.serial_number)
|
|
1197
|
+
self.assertEqual(5000, self.rated_power)
|
|
1198
|
+
self.assertEqual(0, self.modbus_version)
|
|
1199
|
+
self.assertEqual(254, self.ac_output_type)
|
|
1200
|
+
self.assertEqual(3, self.dsp1_version)
|
|
1201
|
+
self.assertEqual(3, self.dsp2_version)
|
|
1202
|
+
self.assertEqual(124, self.dsp_svn_version)
|
|
1203
|
+
self.assertEqual(11, self.arm_version)
|
|
1204
|
+
self.assertEqual(147, self.arm_svn_version)
|
|
1205
|
+
self.assertEqual('04029-03-S10', self.firmware)
|
|
1206
|
+
self.assertEqual('02041-11-S00', self.arm_firmware)
|
|
@@ -155,6 +155,16 @@ class TestUtils(TestCase):
|
|
|
155
155
|
data = MockResponse("ffffffff")
|
|
156
156
|
self.assertIsNone(testee.read(data))
|
|
157
157
|
|
|
158
|
+
def test_energy8(self):
|
|
159
|
+
testee = Energy8("", 0, "", None)
|
|
160
|
+
|
|
161
|
+
data = MockResponse("0000000000015b41")
|
|
162
|
+
self.assertEqual(888.97, testee.read(data))
|
|
163
|
+
data = MockResponse("0000000000038E6C")
|
|
164
|
+
self.assertEqual(2330.68, testee.read(data))
|
|
165
|
+
data = MockResponse("ffffffffffffffff")
|
|
166
|
+
self.assertIsNone(testee.read(data))
|
|
167
|
+
|
|
158
168
|
def test_timestamp(self):
|
|
159
169
|
testee = Timestamp("", 0, "", None)
|
|
160
170
|
|
goodwe-0.3.4/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.4
|
|
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
|