From 6a128084969e31ce66e7c51004e56a479d0dc9e9 Mon Sep 17 00:00:00 2001 From: mle Date: Sun, 3 Dec 2023 17:39:53 +0100 Subject: [PATCH] Add support for MPTT sensors on ETT inverters Add support for MPTT sensors on ETT >25K inverters --- goodwe/et.py | 93 +++++++++++++++++++++++--- goodwe/inverter.py | 2 +- tests/inverter_check.py | 2 +- tests/sample/et/GW25K-ET_mptt_data.hex | 1 + tests/test_et.py | 51 +++++++++++++- 5 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 tests/sample/et/GW25K-ET_mptt_data.hex diff --git a/goodwe/et.py b/goodwe/et.py index e577292..b311e8e 100644 --- a/goodwe/et.py +++ b/goodwe/et.py @@ -257,6 +257,65 @@ class ET(Inverter): Integer("meter_sw_version", 88, "Meter Software Version", "", Kind.GRID), # 36044 ) + # Inverter's MPPT data + # Modbus registers from offset 0x89e5 (35301) + __all_sensors_mptt: Tuple[Sensor, ...] = ( + Power4("ppv_total", 0, "PV Power Total", Kind.PV), # 35301 + # 35303 PV channel RO U16 1 1 PV channel + Voltage("vpv5", 6, "PV5 Voltage", Kind.PV), # 35304 + Current("ipv5", 8, "PV5 Current", Kind.PV), # 35305 + Voltage("vpv6", 10, "PV6 Voltage", Kind.PV), # 35306 + Current("ipv6", 12, "PV6 Current", Kind.PV), # 35307 + Voltage("vpv7", 14, "PV7 Voltage", Kind.PV), # 35308 + Current("ipv7", 16, "PV7 Current", Kind.PV), # 35309 + Voltage("vpv8", 18, "PV8 Voltage", Kind.PV), # 35310 + Current("ipv8", 20, "PV8 Current", Kind.PV), # 35311 + Voltage("vpv9", 22, "PV9 Voltage", Kind.PV), # 35312 + Current("ipv9", 24, "PV9 Current", Kind.PV), # 35313 + Voltage("vpv10", 26, "PV10 Voltage", Kind.PV), # 35314 + Current("ipv10", 28, "PV10 Current", Kind.PV), # 35315 + Voltage("vpv11", 30, "PV11 Voltage", Kind.PV), # 35316 + Current("ipv11", 32, "PV11 Current", Kind.PV), # 35317 + Voltage("vpv12", 34, "PV12 Voltage", Kind.PV), # 35318 + Current("ipv12", 36, "PV12 Current", Kind.PV), # 35319 + Voltage("vpv13", 38, "PV13 Voltage", Kind.PV), # 35320 + Current("ipv13", 40, "PV13 Current", Kind.PV), # 35321 + Voltage("vpv14", 42, "PV14 Voltage", Kind.PV), # 35322 + Current("ipv14", 44, "PV14 Current", Kind.PV), # 35323 + Voltage("vpv15", 46, "PV15 Voltage", Kind.PV), # 35324 + Current("ipv15", 48, "PV15 Current", Kind.PV), # 35325 + Voltage("vpv16", 50, "PV16 Voltage", Kind.PV), # 35326 + Current("ipv16", 52, "PV16 Current", Kind.PV), # 35327 + # 35328 Warning Message + # 35330 Grid10minAvgVoltR + # 35331 Grid10minAvgVoltS + # 35332 Grid10minAvgVoltT + # 35333 Error Message Extend + # 35335 Warning Message Extend + Power("pmppt1", 72, "MPPT1 Power", Kind.PV), # 35337 + Power("pmppt2", 74, "MPPT2 Power", Kind.PV), # 35338 + Power("pmppt3", 76, "MPPT3 Power", Kind.PV), # 35339 + Power("pmppt4", 78, "MPPT4 Power", Kind.PV), # 35340 + Power("pmppt5", 80, "MPPT5 Power", Kind.PV), # 35341 + Power("pmppt6", 82, "MPPT6 Power", Kind.PV), # 35342 + Power("pmppt7", 84, "MPPT7 Power", Kind.PV), # 35343 + Power("pmppt8", 86, "MPPT8 Power", Kind.PV), # 35344 + Power("imppt1", 88, "MPPT1 Current", Kind.PV), # 35345 + Power("imppt2", 90, "MPPT2 Current", Kind.PV), # 35346 + Power("imppt3", 92, "MPPT3 Current", Kind.PV), # 35347 + Power("imppt4", 94, "MPPT4 Current", Kind.PV), # 35348 + Power("imppt5", 96, "MPPT5 Current", Kind.PV), # 35349 + Power("imppt6", 98, "MPPT6 Current", Kind.PV), # 35350 + Power("imppt7", 100, "MPPT7 Current", Kind.PV), # 35351 + Power("imppt8", 102, "MPPT8 Current", Kind.PV), # 35352 + Reactive4("reactive_power1", 104, "Reactive Power L1", Kind.GRID), # 36353/54 + Reactive4("reactive_power2", 108, "Reactive Power L2", Kind.GRID), # 36355/56 + Reactive4("reactive_power3", 112, "Reactive Power L2", Kind.GRID), # 36357/58 + Apparent4("apparent_power1", 116, "Apparent Power L1", Kind.GRID), # 36359/60 + Apparent4("apparent_power2", 120, "Apparent Power L2", Kind.GRID), # 36361/62 + Apparent4("apparent_power3", 124, "Apparent Power L3", Kind.GRID), # 36363/64 + ) + # Modbus registers of inverter settings, offsets are modbus register addresses __all_settings: Tuple[Sensor, ...] = ( Integer("comm_address", 45127, "Communication Address", ""), @@ -342,12 +401,15 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int self._READ_METER_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x2d) self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018) self._READ_BATTERY2_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9858, 0x0016) + self._READ_MPTT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89a5, 0x3d) self._has_battery: bool = True self._has_battery2: bool = False + self._has_mptt: bool = False self._sensors = self.__all_sensors self._sensors_battery = self.__all_sensors_battery self._sensors_battery2 = self.__all_sensors_battery2 self._sensors_meter = self.__all_sensors_meter + self._sensors_mptt = self.__all_sensors_mptt self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings} def _supports_eco_mode_v2(self) -> bool: @@ -390,12 +452,15 @@ async def read_device_info(self): self._sensors = tuple(filter(self._single_phase_only, self._sensors)) self._sensors_meter = tuple(filter(self._single_phase_only, self._sensors_meter)) - if is_2_battery(self) or self.rated_power > 25000: + if is_2_battery(self) or self.rated_power >= 25000: self._has_battery2 = True - if self.arm_version >= 19 or self.rated_power > 15000: + if self.rated_power >= 15000: + self._has_mptt = True + + if self.arm_version >= 19 or self.rated_power >= 15000: self._settings.update({s.id_: s for s in self.__settings_arm_fw_19}) - if self.arm_version >= 22 or self.rated_power > 15000: + if self.arm_version >= 22 or self.rated_power >= 15000: self._settings.update({s.id_: s for s in self.__settings_arm_fw_22}) async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: @@ -422,6 +487,16 @@ async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict raw_data = await self._read_from_socket(self._READ_METER_DATA) data.update(self._map_response(raw_data[5:-2], self._sensors_meter, include_unknown_sensors)) + + if self._has_mptt: + try: + raw_data = await self._read_from_socket(self._READ_MPTT_DATA) + data.update(self._map_response(raw_data[5:-2], self._sensors_mptt, include_unknown_sensors)) + except RequestRejectedException as ex: + if ex.message == 'ILLEGAL DATA ADDRESS': + logger.warning("Cannot read MPPT values, disabling further attempts.") + self._has_mptt = False + return data async def read_setting(self, setting_id: str) -> Any: @@ -533,12 +608,14 @@ async def set_ongrid_battery_dod(self, dod: int) -> None: await self.write_setting('battery_discharge_depth', 100 - dod) def sensors(self) -> Tuple[Sensor, ...]: + result = self._sensors + self._sensors_meter + if self._has_battery: + result = result + self._sensors_battery if self._has_battery2: - return self._sensors + self._sensors_battery + self._sensors_battery2 + self._sensors_meter - elif self._has_battery: - return self._sensors + self._sensors_battery + self._sensors_meter - else: - return self._sensors + self._sensors_meter + result = result + self._sensors_battery2 + if self._has_mptt: + result = result + self._sensors_mptt + return result def settings(self) -> Tuple[Sensor, ...]: return tuple(self._settings.values()) diff --git a/goodwe/inverter.py b/goodwe/inverter.py index de20ede..bf8ec53 100644 --- a/goodwe/inverter.py +++ b/goodwe/inverter.py @@ -98,7 +98,7 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int self.model_name: str | None = None self.serial_number: str | None = None - self.rated_power: int | None = None + self.rated_power: int = 0 self.ac_output_type: int | None = None self.firmware: str | None = None self.arm_firmware: str | None = None diff --git a/tests/inverter_check.py b/tests/inverter_check.py index d98b1ff..7740cf4 100644 --- a/tests/inverter_check.py +++ b/tests/inverter_check.py @@ -14,7 +14,7 @@ ) # Set the appropriate IP address -IP_ADDRESS = "192.168.1.14" +IP_ADDRESS = "192.168.2.14" FAMILY = "ET" # One of ET, EH, ES, EM, DT, NS, XS or None to detect family automatically COMM_ADDR = 0xf7 # Usually 0xf7 for ET/EH/EM/ES or 0x7f for DT/D-NS/XS, or None for default value TIMEOUT = 1 diff --git a/tests/sample/et/GW25K-ET_mptt_data.hex b/tests/sample/et/GW25K-ET_mptt_data.hex new file mode 100644 index 0000000..559cba4 --- /dev/null +++ b/tests/sample/et/GW25K-ET_mptt_data.hex @@ -0,0 +1 @@ +aa55f7037a0000021100020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009030903090300000000ffffffff00e8012b00000000000000000000000000030004000000000000000000000000000000000000000000000000000000000000026e \ No newline at end of file diff --git a/tests/test_et.py b/tests/test_et.py index 136d489..44ed226 100644 --- a/tests/test_et.py +++ b/tests/test_et.py @@ -890,6 +890,8 @@ def __init__(self, methodName='runTest'): self.mock_response(self._READ_RUNNING_DATA, 'GW25K-ET_running_data.hex') self.mock_response(self._READ_METER_DATA, 'GW25K-ET_meter_data.hex') self.mock_response(self._READ_BATTERY_INFO, 'GW25K-ET_battery_info.hex') + # self.mock_response(self._READ_BATTERY_INFO2, 'GW25K-ET_battery2_info.hex') + self.mock_response(self._READ_MPTT_DATA, 'GW25K-ET_mptt_data.hex') def test_GW25K_ET_device_info(self): self.loop.run_until_complete(self.read_device_info()) @@ -912,7 +914,7 @@ def test_GW25K_ET_runtime_data(self): self.sensor_map = {s.id_: s.unit for s in self.sensors()} data = self.loop.run_until_complete(self.read_runtime_data(True)) - self.assertEqual(174, len(data)) + self.assertEqual(221, len(data)) 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) @@ -1090,5 +1092,52 @@ def test_GW25K_ET_runtime_data(self): self.assertSensor('meter_apparent_power_total', 1657, 'VA', data) self.assertSensor('meter_type', 2, '', data) self.assertSensor('meter_sw_version', 5, '', data) + self.assertSensor('ppv_total', 529, 'W', data) + self.assertSensor('vpv5', 0.0, 'V', data) + self.assertSensor('ipv5', 0.0, 'A', data) + self.assertSensor('vpv6', 0.0, 'V', data) + self.assertSensor('ipv6', 0.0, 'A', data) + self.assertSensor('vpv7', 0.0, 'V', data) + self.assertSensor('ipv7', 0.0, 'A', data) + self.assertSensor('vpv8', 0.0, 'V', data) + self.assertSensor('ipv8', 0.0, 'A', data) + self.assertSensor('vpv9', 0.0, 'V', data) + self.assertSensor('ipv9', 0.0, 'A', data) + self.assertSensor('vpv10', 0.0, 'V', data) + self.assertSensor('ipv10', 0.0, 'A', data) + self.assertSensor('vpv11', 0.0, 'V', data) + self.assertSensor('ipv11', 0.0, 'A', data) + self.assertSensor('vpv12', 0.0, 'V', data) + self.assertSensor('ipv12', 0.0, 'A', data) + self.assertSensor('vpv13', 0.0, 'V', data) + self.assertSensor('ipv13', 0.0, 'A', data) + self.assertSensor('vpv14', 0.0, 'V', data) + self.assertSensor('ipv14', 0.0, 'A', data) + self.assertSensor('vpv15', 0.0, 'V', data) + self.assertSensor('ipv15', 0.0, 'A', data) + self.assertSensor('vpv16', 0.0, 'V', data) + self.assertSensor('ipv16', 0.0, 'A', data) + self.assertSensor('pmppt1', 232, 'W', data) + self.assertSensor('pmppt2', 299, 'W', data) + self.assertSensor('pmppt3', 0, 'W', data) + self.assertSensor('pmppt4', 0, 'W', data) + self.assertSensor('pmppt5', 0, 'W', data) + self.assertSensor('pmppt6', 0, 'W', data) + self.assertSensor('pmppt7', 0, 'W', data) + self.assertSensor('pmppt8', 0, 'W', data) + self.assertSensor('imppt1', 3, 'W', data) + self.assertSensor('imppt2', 4, 'W', data) + self.assertSensor('imppt3', 0, 'W', data) + self.assertSensor('imppt4', 0, 'W', data) + self.assertSensor('imppt5', 0, 'W', data) + self.assertSensor('imppt6', 0, 'W', data) + self.assertSensor('imppt7', 0, 'W', data) + self.assertSensor('imppt8', 0, 'W', data) + self.assertSensor('reactive_power1', 0, 'var', data) + self.assertSensor('reactive_power2', 0, 'var', data) + self.assertSensor('reactive_power3', 0, 'var', data) + self.assertSensor('apparent_power1', 0, 'VA', data) + self.assertSensor('apparent_power2', 0, 'VA', data) + self.assertSensor('apparent_power3', 0, 'VA', data) self.assertFalse(self.sensor_map, f"Some sensors were not tested {self.sensor_map}")