From 6890df873c2b32738c9a1ca2eda65e04cf5c249b Mon Sep 17 00:00:00 2001 From: mle Date: Sat, 8 Jun 2024 15:52:47 +0200 Subject: [PATCH] Add meter_e_total_exp (L1/L2/L3) to ETT/745 inverters --- goodwe/et.py | 38 ++++++++++++++++++++++++++--- goodwe/model.py | 4 ++++ goodwe/sensor.py | 19 +++++++++++++++ tests/test_et.py | 57 ++++++++++++++++---------------------------- tests/test_sensor.py | 10 ++++++++ 5 files changed, 88 insertions(+), 40 deletions(-) diff --git a/goodwe/et.py b/goodwe/et.py index 506ea01..af87019 100644 --- a/goodwe/et.py +++ b/goodwe/et.py @@ -257,7 +257,8 @@ class ET(Inverter): Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID), Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit) Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID), - # Sensors added in some ARM fw update, read when flag _has_meter_extended is on + + # Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID), Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID), Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID), @@ -268,6 +269,15 @@ class ET(Inverter): Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID), Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID), Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID), + + Energy8("meter_e_total_exp1", 36092, "Meter Total Energy (export) L1", Kind.GRID), + Energy8("meter_e_total_exp2", 36096, "Meter Total Energy (export) L2", Kind.GRID), + Energy8("meter_e_total_exp3", 36100, "Meter Total Energy (export) L3", Kind.GRID), + Energy8("meter_e_total_exp", 36104, "Meter Total Energy (export)", Kind.GRID), + Energy8("meter_e_total_imp1", 36108, "Meter Total Energy (import) L1", Kind.GRID), + Energy8("meter_e_total_imp2", 36112, "Meter Total Energy (import) L2", Kind.GRID), + Energy8("meter_e_total_imp3", 36116, "Meter Total Energy (import) L3", Kind.GRID), + Energy8("meter_e_total_imp", 36120, "Meter Total Energy (import)", Kind.GRID), ) # Inverter's MPPT data @@ -464,6 +474,7 @@ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, r self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d) self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d) self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a) + self._READ_METER_DATA_EXTENDED2: ProtocolCommand = self._read_command(0x8ca0, 0x7d) self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018) self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016) 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 self._has_battery: bool = True self._has_battery2: bool = False self._has_meter_extended: bool = False + self._has_meter_extended2: bool = False self._has_mppt: bool = False self._sensors = self.__all_sensors self._sensors_battery = self.__all_sensors_battery @@ -490,6 +502,11 @@ def _not_extended_meter(s: Sensor) -> bool: """Filter to exclude extended meter sensors""" return s.offset < 36045 + @staticmethod + def _not_extended_meter2(s: Sensor) -> bool: + """Filter to exclude extended meter sensors""" + return s.offset < 36058 + async def read_device_info(self): response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) response = response.response_data() @@ -520,9 +537,10 @@ async def read_device_info(self): if is_2_battery(self) or self.rated_power >= 25000: self._has_battery2 = True - if self.rated_power >= 15000: + if is_745_platform(self) or self.rated_power >= 15000: self._has_mppt = True self._has_meter_extended = True + self._has_meter_extended2 = True else: self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter)) @@ -577,7 +595,21 @@ async def read_runtime_data(self) -> Dict[str, Any]: else: raise ex - if self._has_meter_extended: + if self._has_meter_extended2: + try: + response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2) + data.update(self._map_response(response, self._sensors_meter)) + except RequestRejectedException as ex: + if ex.message == ILLEGAL_DATA_ADDRESS: + logger.info("Extended meter values not supported, disabling further attempts.") + self._has_meter_extended2 = False + self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter)) + response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED) + data.update( + self._map_response(response, self._sensors_meter)) + else: + raise ex + elif self._has_meter_extended: try: response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED) data.update(self._map_response(response, self._sensors_meter)) diff --git a/goodwe/model.py b/goodwe/model.py index d08cf56..95a2ab9 100644 --- a/goodwe/model.py +++ b/goodwe/model.py @@ -48,3 +48,7 @@ def is_2_battery(inverter: Inverter) -> bool: def is_745_platform(inverter: Inverter) -> bool: return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) or any( model in inverter.serial_number for model in PLATFORM_745_HV_MODELS) + + +def is_753_platform(inverter: Inverter) -> bool: + return any(model in inverter.serial_number for model in PLATFORM_753_MODELS) diff --git a/goodwe/sensor.py b/goodwe/sensor.py index 92cb55c..3005834 100644 --- a/goodwe/sensor.py +++ b/goodwe/sensor.py @@ -197,6 +197,17 @@ def read_value(self, data: ProtocolResponse): return float(value) / 10 if value is not None else None +class Energy8(Sensor): + """Sensor representing energy [kWh] value encoded in 8 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 8, "kWh", kind) + + def read_value(self, data: ProtocolResponse): + value = read_bytes8(data) + return float(value) / 100 if value is not None else None + + class Apparent(Sensor): """Sensor representing apparent power [VA] value encoded in 2 bytes""" @@ -840,6 +851,14 @@ def read_bytes4_signed(buffer: ProtocolResponse, offset: int = None) -> int: return int.from_bytes(buffer.read(4), byteorder="big", signed=True) +def read_bytes8(buffer: ProtocolResponse, offset: int = None, undef: int = None) -> int: + """Retrieve 8 byte (unsigned int) value from buffer""" + if offset is not None: + buffer.seek(offset) + value = int.from_bytes(buffer.read(8), byteorder="big", signed=False) + return undef if value == 0xffffffffffffffff else value + + def read_decimal2(buffer: ProtocolResponse, scale: int, offset: int = None) -> float: """Retrieve 2 byte (signed float) value from buffer""" if offset is not None: diff --git a/tests/test_et.py b/tests/test_et.py index fa2abde..b5496a1 100644 --- a/tests/test_et.py +++ b/tests/test_et.py @@ -15,7 +15,7 @@ class EtMock(TestCase, ET): def __init__(self, methodName='runTest'): TestCase.__init__(self, methodName) ET.__init__(self, "localhost", 8899) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} + self.sensor_map = {s.id_: s for s in self.sensors()} self._mock_responses = {} self._list_of_requests = [] @@ -41,10 +41,11 @@ async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse: self._list_of_requests.append(command.request) return ProtocolResponse(bytes.fromhex("aa55f700010203040506070809"), command) - def assertSensor(self, sensor, expected_value, expected_unit, data): - self.assertEqual(expected_value, data.get(sensor)) - self.assertEqual(expected_unit, self.sensor_map.get(sensor)) - self.sensor_map.pop(sensor) + def assertSensor(self, sensor_name, expected_value, expected_unit, data): + self.assertEqual(expected_value, data.get(sensor_name)) + sensor = self.sensor_map.get(sensor_name); + self.assertEqual(expected_unit, sensor.unit) + self.sensor_map.pop(sensor_name) @classmethod def setUpClass(cls): @@ -81,13 +82,15 @@ def test_GW10K_ET_device_info(self): def test_GW10K_ET_runtime_data(self): # Reset sensors self.loop.run_until_complete(self.read_device_info()) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} + self.sensor_map = {s.id_: s for s in self.sensors()} data = self.loop.run_until_complete(self.read_runtime_data()) self.assertEqual(145, len(data)) + self.assertEqual(36015, self.sensor_map.get("meter_e_total_exp").offset) + # for sensor in self.sensors(): - # print(f"self.assertSensor('{sensor.id_}', {data[sensor.id_]}, '{self.sensor_map.get(sensor.id_)}', data)") + # print(f"self.assertSensor('{sensor.id_}', {data[sensor.id_]}, '{self.sensor_map.get(sensor.id_).unit}', data)") self.assertSensor('timestamp', datetime.strptime('2021-08-22 11:11:12', '%Y-%m-%d %H:%M:%S'), '', data) self.assertSensor('vpv1', 332.6, 'V', data) @@ -386,7 +389,7 @@ def test_GW10K_ET_setting_fw1023(self): def test_GW10K_ET_runtime_data_fw1023(self): # Reset sensors self.loop.run_until_complete(self.read_device_info()) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} + self.sensor_map = {s.id_: s for s in self.sensors()} data = self.loop.run_until_complete(self.read_runtime_data()) self.assertEqual(145, len(data)) @@ -596,7 +599,7 @@ def test_GEH10_1U_10_device_info(self): def test_GEH10_1U_10_runtime_data(self): # Reset sensors self.loop.run_until_complete(self.read_device_info()) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} + self.sensor_map = {s.id_: s for s in self.sensors()} data = self.loop.run_until_complete(self.read_runtime_data()) self.assertEqual(125, len(data)) @@ -760,6 +763,7 @@ def __init__(self, methodName='runTest'): EtMock.__init__(self, methodName) self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW25K-ET_device_info.hex') self.mock_response(self._READ_RUNNING_DATA, 'GW25K-ET_running_data.hex') + self.mock_response(self._READ_METER_DATA_EXTENDED2, ILLEGAL_DATA_ADDRESS) self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW25K-ET_meter_data.hex') self.mock_response(self._READ_BATTERY_INFO, 'GW25K-ET_battery_info.hex') self.mock_response(self._READ_MPPT_DATA, 'GW25K-ET_mppt_data.hex') @@ -782,11 +786,14 @@ def test_GW25K_ET_device_info(self): def test_GW25K_ET_runtime_data(self): # Reset sensors self.loop.run_until_complete(self.read_device_info()) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} data = self.loop.run_until_complete(self.read_runtime_data()) self.assertEqual(237, len(data)) + self.sensor_map = {s.id_: s for s in self.sensors()} + + # self.assertEqual(36104, self.sensor_map.get("meter_e_total_exp").offset) + self.assertSensor('timestamp', datetime.strptime('2023-12-03 14:07:07', '%Y-%m-%d %H:%M:%S'), '', data) self.assertSensor('vpv1', 737.9, 'V', data) self.assertSensor('ipv1', 1.4, 'A', data) @@ -1036,6 +1043,7 @@ def __init__(self, methodName='runTest'): EtMock.__init__(self, methodName) self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW29K9-ET_device_info.hex') self.mock_response(self._READ_RUNNING_DATA, 'GW29K9-ET_running_data.hex') + self.mock_response(self._READ_METER_DATA_EXTENDED2, ILLEGAL_DATA_ADDRESS) self.mock_response(self._READ_METER_DATA_EXTENDED, 'GW29K9-ET_meter_data.hex') self.mock_response(self._READ_BATTERY_INFO, 'GW29K9-ET_battery_info.hex') self.mock_response(self._READ_BATTERY2_INFO, 'GW29K9-ET_battery2_info.hex') @@ -1059,11 +1067,12 @@ def test_GW29K9_ET_device_info(self): def test_GW29K9_ET_runtime_data(self): # Reset sensors self.loop.run_until_complete(self.read_device_info()) - self.sensor_map = {s.id_: s.unit for s in self.sensors()} data = self.loop.run_until_complete(self.read_runtime_data()) self.assertEqual(211, len(data)) + self.sensor_map = {s.id_: s for s in self.sensors()} + self.assertSensor('timestamp', datetime.strptime('2024-01-17 14:49:14', '%Y-%m-%d %H:%M:%S'), '', data) self.assertSensor('vpv1', 682.9, 'V', data) self.assertSensor('ipv1', 1.5, 'A', data) @@ -1206,32 +1215,6 @@ def test_GW29K9_ET_runtime_data(self): self.assertSensor('meter_current1', 4.6, 'A', data) self.assertSensor('meter_current2', 6.0, 'A', data) self.assertSensor('meter_current3', 13.6, 'A', data) - self.assertSensor('battery_bms', None, '', data) - self.assertSensor('battery_index', None, '', data) - self.assertSensor('battery_status', None, '', data) - self.assertSensor('battery_temperature', None, 'C', data) - self.assertSensor('battery_charge_limit', None, 'A', data) - self.assertSensor('battery_discharge_limit', None, 'A', data) - self.assertSensor('battery_error_l', None, '', data) - self.assertSensor('battery_soc', None, '%', data) - self.assertSensor('battery_soh', None, '%', data) - self.assertSensor('battery_modules', None, '', data) - self.assertSensor('battery_warning_l', None, '', data) - self.assertSensor('battery_protocol', None, '', data) - self.assertSensor('battery_error_h', None, '', data) - self.assertSensor('battery_error', None, '', data) - self.assertSensor('battery_warning_h', None, '', data) - self.assertSensor('battery_warning', None, '', data) - self.assertSensor('battery_sw_version', None, '', data) - self.assertSensor('battery_hw_version', None, '', data) - self.assertSensor('battery_max_cell_temp_id', None, '', data) - self.assertSensor('battery_min_cell_temp_id', None, '', data) - self.assertSensor('battery_max_cell_voltage_id', None, '', data) - self.assertSensor('battery_min_cell_voltage_id', None, '', data) - self.assertSensor('battery_max_cell_temp', None, 'C', data) - self.assertSensor('battery_min_cell_temp', None, 'C', data) - self.assertSensor('battery_max_cell_voltage', None, 'V', data) - self.assertSensor('battery_min_cell_voltage', None, 'V', data) self.assertSensor('battery2_status', 0, '', data) self.assertSensor('battery2_temperature', 0.0, 'C', data) self.assertSensor('battery2_charge_limit', 0, 'A', data) diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 657e07a..3409c64 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -169,6 +169,16 @@ def test_energy4(self): data = MockResponse("ffffffff") self.assertIsNone(testee.read(data)) + def test_energy8(self): + testee = Energy8("", 0, "", None) + + data = MockResponse("0000000000015b41") + self.assertEqual(888.97, testee.read(data)) + data = MockResponse("0000000000038E6C") + self.assertEqual(2330.68, testee.read(data)) + data = MockResponse("ffffffffffffffff") + self.assertIsNone(testee.read(data)) + def test_temp(self): testee = Temp("", 0, "", None)