Skip to content

Commit 6890df8

Browse files
committed
Add meter_e_total_exp (L1/L2/L3) to ETT/745 inverters
1 parent e48e6e6 commit 6890df8

File tree

5 files changed

+88
-40
lines changed

5 files changed

+88
-40
lines changed

goodwe/et.py

+35-3
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ class ET(Inverter):
257257
Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
258258
Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
259259
Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
260-
# Sensors added in some ARM fw update, read when flag _has_meter_extended is on
260+
261+
# Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on
261262
Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
262263
Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
263264
Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
@@ -268,6 +269,15 @@ class ET(Inverter):
268269
Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
269270
Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
270271
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),
271281
)
272282

273283
# Inverter's MPPT data
@@ -464,6 +474,7 @@ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, r
464474
self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
465475
self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
466476
self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
477+
self._READ_METER_DATA_EXTENDED2: ProtocolCommand = self._read_command(0x8ca0, 0x7d)
467478
self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
468479
self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
469480
self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
@@ -472,6 +483,7 @@ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, r
472483
self._has_battery: bool = True
473484
self._has_battery2: bool = False
474485
self._has_meter_extended: bool = False
486+
self._has_meter_extended2: bool = False
475487
self._has_mppt: bool = False
476488
self._sensors = self.__all_sensors
477489
self._sensors_battery = self.__all_sensors_battery
@@ -490,6 +502,11 @@ def _not_extended_meter(s: Sensor) -> bool:
490502
"""Filter to exclude extended meter sensors"""
491503
return s.offset < 36045
492504

505+
@staticmethod
506+
def _not_extended_meter2(s: Sensor) -> bool:
507+
"""Filter to exclude extended meter sensors"""
508+
return s.offset < 36058
509+
493510
async def read_device_info(self):
494511
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
495512
response = response.response_data()
@@ -520,9 +537,10 @@ async def read_device_info(self):
520537
if is_2_battery(self) or self.rated_power >= 25000:
521538
self._has_battery2 = True
522539

523-
if self.rated_power >= 15000:
540+
if is_745_platform(self) or self.rated_power >= 15000:
524541
self._has_mppt = True
525542
self._has_meter_extended = True
543+
self._has_meter_extended2 = True
526544
else:
527545
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
528546

@@ -577,7 +595,21 @@ async def read_runtime_data(self) -> Dict[str, Any]:
577595
else:
578596
raise ex
579597

580-
if self._has_meter_extended:
598+
if self._has_meter_extended2:
599+
try:
600+
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2)
601+
data.update(self._map_response(response, self._sensors_meter))
602+
except RequestRejectedException as ex:
603+
if ex.message == ILLEGAL_DATA_ADDRESS:
604+
logger.info("Extended meter values not supported, disabling further attempts.")
605+
self._has_meter_extended2 = False
606+
self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter))
607+
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
608+
data.update(
609+
self._map_response(response, self._sensors_meter))
610+
else:
611+
raise ex
612+
elif self._has_meter_extended:
581613
try:
582614
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
583615
data.update(self._map_response(response, self._sensors_meter))

goodwe/model.py

+4
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ def is_2_battery(inverter: Inverter) -> bool:
4848
def is_745_platform(inverter: Inverter) -> bool:
4949
return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any(
5050
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)

goodwe/sensor.py

+19
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,17 @@ def read_value(self, data: ProtocolResponse):
197197
return float(value) / 10 if value is not None else None
198198

199199

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+
200211
class Apparent(Sensor):
201212
"""Sensor representing apparent power [VA] value encoded in 2 bytes"""
202213

@@ -840,6 +851,14 @@ def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int:
840851
return int.from_bytes(buffer.read(4), byteorder="big", signed=True)
841852

842853

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+
843862
def read_decimal2(buffer: ProtocolResponse, scale: int, offset: int = None) -> float:
844863
"""Retrieve 2 byte (signed float) value from buffer"""
845864
if offset is not None:

tests/test_et.py

+20-37
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class EtMock(TestCase, ET):
1515
def __init__(self, methodName='runTest'):
1616
TestCase.__init__(self, methodName)
1717
ET.__init__(self, "localhost", 8899)
18-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
18+
self.sensor_map = {s.id_: s for s in self.sensors()}
1919
self._mock_responses = {}
2020
self._list_of_requests = []
2121

@@ -41,10 +41,11 @@ async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
4141
self._list_of_requests.append(command.request)
4242
return ProtocolResponse(bytes.fromhex("aa55f700010203040506070809"), command)
4343

44-
def assertSensor(self, sensor, expected_value, expected_unit, data):
45-
self.assertEqual(expected_value, data.get(sensor))
46-
self.assertEqual(expected_unit, self.sensor_map.get(sensor))
47-
self.sensor_map.pop(sensor)
44+
def assertSensor(self, sensor_name, expected_value, expected_unit, data):
45+
self.assertEqual(expected_value, data.get(sensor_name))
46+
sensor = self.sensor_map.get(sensor_name);
47+
self.assertEqual(expected_unit, sensor.unit)
48+
self.sensor_map.pop(sensor_name)
4849

4950
@classmethod
5051
def setUpClass(cls):
@@ -81,13 +82,15 @@ def test_GW10K_ET_device_info(self):
8182
def test_GW10K_ET_runtime_data(self):
8283
# Reset sensors
8384
self.loop.run_until_complete(self.read_device_info())
84-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
85+
self.sensor_map = {s.id_: s for s in self.sensors()}
8586

8687
data = self.loop.run_until_complete(self.read_runtime_data())
8788
self.assertEqual(145, len(data))
8889

90+
self.assertEqual(36015, self.sensor_map.get("meter_e_total_exp").offset)
91+
8992
# 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)")
9194

9295
self.assertSensor('timestamp', datetime.strptime('2021-08-22 11:11:12', '%Y-%m-%d %H:%M:%S'), '', data)
9396
self.assertSensor('vpv1', 332.6, 'V', data)
@@ -386,7 +389,7 @@ def test_GW10K_ET_setting_fw1023(self):
386389
def test_GW10K_ET_runtime_data_fw1023(self):
387390
# Reset sensors
388391
self.loop.run_until_complete(self.read_device_info())
389-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
392+
self.sensor_map = {s.id_: s for s in self.sensors()}
390393

391394
data = self.loop.run_until_complete(self.read_runtime_data())
392395
self.assertEqual(145, len(data))
@@ -596,7 +599,7 @@ def test_GEH10_1U_10_device_info(self):
596599
def test_GEH10_1U_10_runtime_data(self):
597600
# Reset sensors
598601
self.loop.run_until_complete(self.read_device_info())
599-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
602+
self.sensor_map = {s.id_: s for s in self.sensors()}
600603

601604
data = self.loop.run_until_complete(self.read_runtime_data())
602605
self.assertEqual(125, len(data))
@@ -760,6 +763,7 @@ def __init__(self, methodName='runTest'):
760763
EtMock.__init__(self, methodName)
761764
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW25K-ET_device_info.hex')
762765
self.mock_response(self._READ_RUNNING_DATA, 'GW25K-ET_running_data.hex')
766+
self.mock_response(self._READ_METER_DATA_EXTENDED2, ILLEGAL_DATA_ADDRESS)
763767
self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW25K-ET_meter_data.hex')
764768
self.mock_response(self._READ_BATTERY_INFO, 'GW25K-ET_battery_info.hex')
765769
self.mock_response(self._READ_MPPT_DATA, 'GW25K-ET_mppt_data.hex')
@@ -782,11 +786,14 @@ def test_GW25K_ET_device_info(self):
782786
def test_GW25K_ET_runtime_data(self):
783787
# Reset sensors
784788
self.loop.run_until_complete(self.read_device_info())
785-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
786789

787790
data = self.loop.run_until_complete(self.read_runtime_data())
788791
self.assertEqual(237, len(data))
789792

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+
790797
self.assertSensor('timestamp', datetime.strptime('2023-12-03 14:07:07', '%Y-%m-%d %H:%M:%S'), '', data)
791798
self.assertSensor('vpv1', 737.9, 'V', data)
792799
self.assertSensor('ipv1', 1.4, 'A', data)
@@ -1036,6 +1043,7 @@ def __init__(self, methodName='runTest'):
10361043
EtMock.__init__(self, methodName)
10371044
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW29K9-ET_device_info.hex')
10381045
self.mock_response(self._READ_RUNNING_DATA, 'GW29K9-ET_running_data.hex')
1046+
self.mock_response(self._READ_METER_DATA_EXTENDED2, ILLEGAL_DATA_ADDRESS)
10391047
self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW29K9-ET_meter_data.hex')
10401048
self.mock_response(self._READ_BATTERY_INFO, 'GW29K9-ET_battery_info.hex')
10411049
self.mock_response(self._READ_BATTERY2_INFO, 'GW29K9-ET_battery2_info.hex')
@@ -1059,11 +1067,12 @@ def test_GW29K9_ET_device_info(self):
10591067
def test_GW29K9_ET_runtime_data(self):
10601068
# Reset sensors
10611069
self.loop.run_until_complete(self.read_device_info())
1062-
self.sensor_map = {s.id_: s.unit for s in self.sensors()}
10631070

10641071
data = self.loop.run_until_complete(self.read_runtime_data())
10651072
self.assertEqual(211, len(data))
10661073

1074+
self.sensor_map = {s.id_: s for s in self.sensors()}
1075+
10671076
self.assertSensor('timestamp', datetime.strptime('2024-01-17 14:49:14', '%Y-%m-%d %H:%M:%S'), '', data)
10681077
self.assertSensor('vpv1', 682.9, 'V', data)
10691078
self.assertSensor('ipv1', 1.5, 'A', data)
@@ -1206,32 +1215,6 @@ def test_GW29K9_ET_runtime_data(self):
12061215
self.assertSensor('meter_current1', 4.6, 'A', data)
12071216
self.assertSensor('meter_current2', 6.0, 'A', data)
12081217
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)
12351218
self.assertSensor('battery2_status', 0, '', data)
12361219
self.assertSensor('battery2_temperature', 0.0, 'C', data)
12371220
self.assertSensor('battery2_charge_limit', 0, 'A', data)

tests/test_sensor.py

+10
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ def test_energy4(self):
169169
data = MockResponse("ffffffff")
170170
self.assertIsNone(testee.read(data))
171171

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+
172182
def test_temp(self):
173183
testee = Temp("", 0, "", None)
174184

0 commit comments

Comments
 (0)