From 3214e3325acdd51d56cdff1d4a64123d3c7586e3 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sun, 21 Jun 2026 14:46:26 +0500 Subject: [PATCH] Add support for GW8000M-ES-C10 (ESC platform) ESC was missing from SINGLE_PHASE_MODELS, causing L2/L3 sensors to appear on a single-phase inverter, vpv3/vpv4 to be dropped (745 LV platform has 2 MPPTs), and ghost sensors from large inverters (vpv5-vpv16, pmppt3-pmppt8) to leak through. All three stem from ESC not being recognized. Test data captured live from a GW8000M-ES-C10 with solar active. All 124 existing tests pass. --- .gitignore | 2 + goodwe/et.py | 38 ++++- goodwe/model.py | 5 + .../sample/et/GW8000M-ES-C10_battery_info.hex | 1 + .../sample/et/GW8000M-ES-C10_device_info.hex | 1 + tests/sample/et/GW8000M-ES-C10_meter_data.hex | 1 + tests/sample/et/GW8000M-ES-C10_mppt_data.hex | 1 + .../sample/et/GW8000M-ES-C10_running_data.hex | 1 + tests/test_et.py | 152 ++++++++++++++++++ 9 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 tests/sample/et/GW8000M-ES-C10_battery_info.hex create mode 100644 tests/sample/et/GW8000M-ES-C10_device_info.hex create mode 100644 tests/sample/et/GW8000M-ES-C10_meter_data.hex create mode 100644 tests/sample/et/GW8000M-ES-C10_mppt_data.hex create mode 100644 tests/sample/et/GW8000M-ES-C10_running_data.hex diff --git a/.gitignore b/.gitignore index 2b51766..ab06ef3 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,5 @@ dmypy.json # Version file needed for CI publishing VERSION +# Local MCP server config (contains credentials) +.mcp.json diff --git a/goodwe/et.py b/goodwe/et.py index 8fd84c0..e47b07e 100644 --- a/goodwe/et.py +++ b/goodwe/et.py @@ -8,7 +8,7 @@ from .exceptions import RequestFailedException, RequestRejectedException from .inverter import EMSMode, Inverter, OperationMode, SensorKind as Kind from .modbus import ILLEGAL_DATA_ADDRESS -from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase +from .model import is_2_battery, is_4_mppt, is_745_lv_platform, is_745_platform, is_single_phase from .protocol import ProtocolCommand from .sensor import * @@ -763,9 +763,11 @@ async def read_device_info(self): self.arm_firmware = self._decode(response[54:66]) # 35027 - 35032 if not is_4_mppt(self) and self.rated_power < 15000: - # This inverter does not have 4 MPPTs or PV strings - self._sensors = tuple(filter(lambda s: not ("pv4" in s.id_), self._sensors)) - self._sensors = tuple(filter(lambda s: not ("pv3" in s.id_), self._sensors)) + # platform 745 LV has 2 MPPTs with up to 4 PV strings; keep pv3+pv4 + if not is_745_lv_platform(self): + self._sensors = tuple( + filter(lambda s: "pv3" not in s.id_ and "pv4" not in s.id_, self._sensors) + ) if is_single_phase(self): # this is single phase inverter, filter out all L2 and L3 sensors @@ -773,6 +775,17 @@ async def read_device_info(self): self._sensors_meter = tuple( filter(self._single_phase_only, self._sensors_meter) ) + # For mppt data, only L2/L3 reactive/apparent power are L-phase sensors; + # MPPT channel sensors like pmppt2/imppt2 must stay for 2-MPPT inverters. + self._sensors_mppt = tuple( + filter( + lambda s: s.id_ not in ( + "reactive_power2", "reactive_power3", + "apparent_power2", "apparent_power3", + ), + self._sensors_mppt, + ) + ) if is_2_battery(self) or self.rated_power >= 25000: self._has_battery2 = True @@ -786,6 +799,23 @@ async def read_device_info(self): filter(self._not_extended_meter, self._sensors_meter) ) + if is_745_lv_platform(self): + # platform 745 LV has 2 MPPTs; strip excess channels from larger inverters (vpv5-16, pmppt3-8) + self._sensors_mppt = tuple( + filter( + lambda s: not any( + tag in s.id_ for tag in ( + "vpv5", "vpv6", "vpv7", "vpv8", "vpv9", "vpv10", + "vpv11", "vpv12", "vpv13", "vpv14", "vpv15", "vpv16", + "ipv5", "ipv6", "ipv7", "ipv8", "ipv9", "ipv10", + "ipv11", "ipv12", "ipv13", "ipv14", "ipv15", "ipv16", + "mppt3", "mppt4", "mppt5", "mppt6", "mppt7", "mppt8", + ) + ), + self._sensors_mppt, + ) + ) + # Check and add EcoModeV2 settings added in (ETU fw 19) try: await self._read_from_socket(self._read_command(47547, 6)) diff --git a/goodwe/model.py b/goodwe/model.py index 8b0cd99..4c0dd07 100644 --- a/goodwe/model.py +++ b/goodwe/model.py @@ -88,6 +88,7 @@ "EMN", "ERN", "EBN", + "ESC", "HLB", "HMB", "HBB", @@ -129,6 +130,10 @@ def is_2_battery(inverter: Inverter) -> bool: return any(model in inverter.serial_number for model in BAT_2_MODELS) +def is_745_lv_platform(inverter: Inverter) -> bool: + return any(model in inverter.serial_number for model in PLATFORM_745_LV_MODELS) + + def is_745_platform(inverter: Inverter) -> bool: return any( model in inverter.serial_number for model in PLATFORM_745_LV_MODELS diff --git a/tests/sample/et/GW8000M-ES-C10_battery_info.hex b/tests/sample/et/GW8000M-ES-C10_battery_info.hex new file mode 100644 index 0000000..b2e367e --- /dev/null +++ b/tests/sample/et/GW8000M-ES-C10_battery_info.hex @@ -0,0 +1 @@ +aa55f7033000000100000101540069006900000063006400010000000c000000000000000000000000000000000000000000000000f5f2 \ No newline at end of file diff --git a/tests/sample/et/GW8000M-ES-C10_device_info.hex b/tests/sample/et/GW8000M-ES-C10_device_info.hex new file mode 100644 index 0000000..0d13831 --- /dev/null +++ b/tests/sample/et/GW8000M-ES-C10_device_info.hex @@ -0,0 +1 @@ +aa55f703420079226000003538303030455343323534533033363747573630303045533230000500050521000f01ed30343037302d30352d53313330323132302d31352d5330313131 \ No newline at end of file diff --git a/tests/sample/et/GW8000M-ES-C10_meter_data.hex b/tests/sample/et/GW8000M-ES-C10_meter_data.hex new file mode 100644 index 0000000..56f2e5b --- /dev/null +++ b/tests/sample/et/GW8000M-ES-C10_meter_data.hex @@ -0,0 +1 @@ +aa55f703fa00070000000a00010001002f00000000002f008b008203e703e7008213a300003c87000000000000002f00000000000000000000002f0000008b00000000000000000000008b0000010700000000000000000000010700ff00000000000000000000000000000000092800000000000b000000000000ffffffffffffffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000101800000000000000000000000000000000000000000000101800000000000349ae0000000000000000000000000000000000000000000349ae1a06ec3f \ No newline at end of file diff --git a/tests/sample/et/GW8000M-ES-C10_mppt_data.hex b/tests/sample/et/GW8000M-ES-C10_mppt_data.hex new file mode 100644 index 0000000..8e62778 --- /dev/null +++ b/tests/sample/et/GW8000M-ES-C10_mppt_data.hex @@ -0,0 +1 @@ +aa55f7037a000009c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffffffff05590467ffffffffffffffffffffffff0037002bffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5803 \ No newline at end of file diff --git a/tests/sample/et/GW8000M-ES-C10_running_data.hex b/tests/sample/et/GW8000M-ES-C10_running_data.hex new file mode 100644 index 0000000..ee83f32 --- /dev/null +++ b/tests/sample/et/GW8000M-ES-C10_running_data.hex @@ -0,0 +1 @@ +aa55f703fa1a06150e051509ad00370000055809ad00000000046609f0002b000000000000000000000000000002020939006313a30000090500000000000000000000000000000000000000000001000009050000002900000009000008ee0929006113a30001000008c5000000000000000000000000000000000000000000000000000000170000000000000000000008c500000009000001df000001e28880120d120f02360006000000230003000000cd00010201000000000000c61f000000d70000aba500001dd200ae0000018500080000fb3d00e700002a3c002800002a25000a0000000000010000000000000000000001100100000200660000c72b \ No newline at end of file diff --git a/tests/test_et.py b/tests/test_et.py index 834c006..214a2cb 100644 --- a/tests/test_et.py +++ b/tests/test_et.py @@ -871,6 +871,158 @@ def test_GW6000_ES_20_device_info(self): self.assertEqual("02020-05-S01", self.arm_firmware) +class GW8000M_ES_C10_Test(EtMock): + + def __init__(self, methodName="runTest"): + EtMock.__init__(self, methodName) + self.mock_response( + self._READ_DEVICE_VERSION_INFO, "GW8000M-ES-C10_device_info.hex" + ) + self.mock_response(self._READ_RUNNING_DATA, "GW8000M-ES-C10_running_data.hex") + self.mock_response( + self._READ_METER_DATA_EXTENDED2, "GW8000M-ES-C10_meter_data.hex" + ) + self.mock_response(self._READ_BATTERY_INFO, "GW8000M-ES-C10_battery_info.hex") + self.mock_response(self._READ_MPPT_DATA, "GW8000M-ES-C10_mppt_data.hex") + + def test_GW8000M_ES_C10_device_info(self): + self.loop.run_until_complete(self.read_device_info()) + self.assertEqual("GW6000ES20", self.model_name) + self.assertEqual("58000ESC254S0367", self.serial_number) + self.assertEqual(8800, self.rated_power) + self.assertEqual(121, self.modbus_version) + self.assertEqual(0, self.ac_output_type) + self.assertEqual(5, self.dsp1_version) + self.assertEqual(5, self.dsp2_version) + self.assertEqual(1313, self.dsp_svn_version) + self.assertEqual(15, self.arm_version) + self.assertEqual(493, self.arm_svn_version) + self.assertEqual("04070-05-S13", self.firmware) + self.assertEqual("02120-15-S01", self.arm_firmware) + # After device_info, single-phase filtering must have removed L2/L3 sensors + sensor_ids = {s.id_ for s in self._sensors} + self.assertNotIn("vgrid2", sensor_ids) + self.assertNotIn("vgrid3", sensor_ids) + self.assertNotIn("igrid2", sensor_ids) + self.assertNotIn("backup_v2", sensor_ids) + + def test_GW8000M_ES_C10_runtime_data(self): + self.loop.run_until_complete(self.read_device_info()) + data = self.loop.run_until_complete(self.read_runtime_data()) + self.assertEqual(141, len(data)) + + self.sensor_map = {s.id_: s for s in self.sensors()} + + self.assertSensor( + "timestamp", + datetime.strptime("2026-06-21 14:05:21", "%Y-%m-%d %H:%M:%S"), + "", + data, + ) + self.assertSensor("vpv1", 247.7, "V", data) + self.assertSensor("ipv1", 5.5, "A", data) + self.assertSensor("ppv1", 1368, "W", data) + self.assertSensor("vpv2", 247.7, "V", data) + self.assertSensor("ipv2", 0.0, "A", data) + self.assertSensor("ppv2", 1126, "W", data) + self.assertSensor("vpv3", 254.4, "V", data) + self.assertSensor("ipv3", 4.3, "A", data) + self.assertSensor("ppv3", 0, "W", data) + self.assertSensor("vpv4", 0.0, "V", data) + self.assertSensor("ipv4", 0.0, "A", data) + self.assertSensor("ppv4", 0, "W", data) + self.assertSensor("ppv", 2494, "W", data) + self.assertSensor("pv1_mode", 2, "", data) + self.assertSensor("pv1_mode_label", "PV panels connected, producing power", "", data) + self.assertSensor("pv2_mode", 2, "", data) + self.assertSensor("pv2_mode_label", "PV panels connected, producing power", "", data) + self.assertSensor("pv3_mode", 0, "", data) + self.assertSensor("pv3_mode_label", "PV panels not connected", "", data) + self.assertSensor("pv4_mode", 0, "", data) + self.assertSensor("pv4_mode_label", "PV panels not connected", "", data) + self.assertSensor("vgrid", 236.1, "V", data) + self.assertSensor("igrid", 9.9, "A", data) + self.assertSensor("fgrid", 50.27, "Hz", data) + self.assertSensor("pgrid", 2309, "W", data) + self.assertSensor("grid_mode", 1, "", data) + self.assertSensor("grid_mode_label", "Connected to grid", "", data) + self.assertSensor("total_inverter_power", 2309, "W", data) + self.assertSensor("active_power", 41, "W", data) + self.assertSensor("grid_in_out", 0, "", data) + self.assertSensor("grid_in_out_label", "Idle", "", data) + self.assertSensor("reactive_power", 9, "var", data) + self.assertSensor("apparent_power", 2286, "VA", data) + self.assertSensor("backup_v1", 234.5, "V", data) + self.assertSensor("backup_i1", 9.7, "A", data) + self.assertSensor("backup_f1", 50.27, "Hz", data) + self.assertSensor("load_mode1", 1, "", data) + self.assertSensor("backup_p1", 2245, "W", data) + self.assertSensor("load_p1", 23, "W", data) + self.assertSensor("backup_ptotal", 2245, "W", data) + self.assertSensor("load_ptotal", 9, "W", data) + self.assertSensor("ups_load", 0, "%", data) + self.assertSensor("temperature_air", 47.9, "C", data) + self.assertSensor("temperature_module", 0.0, "C", data) + self.assertSensor("temperature", 48.2, "C", data) + self.assertSensor("bus_voltage", 462.1, "V", data) + self.assertSensor("nbus_voltage", 462.3, "V", data) + self.assertSensor("vbattery1", 56.6, "V", data) + self.assertSensor("ibattery1", 0.6, "A", data) + self.assertSensor("pbattery1", 35, "W", data) + self.assertSensor("battery_mode", 3, "", data) + self.assertSensor("battery_mode_label", "Charge", "", data) + self.assertSensor("warning_code", 0, "", data) + self.assertSensor("safety_country", 205, "", data) + self.assertSensor("safety_country_label", None, "", data) + self.assertSensor("work_mode", 1, "", data) + self.assertSensor("work_mode_label", "Normal (On-Grid)", "", data) + self.assertSensor("error_codes", 0, "", data) + self.assertSensor("errors", "", "", data) + self.assertSensor("e_total", 5071.9, "kWh", data) + self.assertSensor("e_day", 21.5, "kWh", data) + self.assertSensor("e_total_exp", 4394.1, "kWh", data) + self.assertSensor("h_total", 7634, "h", data) + self.assertSensor("e_day_exp", 17.4, "kWh", data) + self.assertSensor("e_total_imp", 38.9, "kWh", data) + self.assertSensor("e_day_imp", 0.8, "kWh", data) + self.assertSensor("e_load_total", 6431.7, "kWh", data) + self.assertSensor("e_load_day", 23.1, "kWh", data) + self.assertSensor("e_bat_charge_total", 1081.2, "kWh", data) + self.assertSensor("e_bat_charge_day", 4.0, "kWh", data) + self.assertSensor("e_bat_discharge_total", 1078.9, "kWh", data) + self.assertSensor("e_bat_discharge_day", 1.0, "kWh", data) + self.assertSensor("diagnose_result", 17826048, "", data) + self.assertSensor( + "diagnose_result_label", + "APP: Discharge current too low, SOC delta too volatile, Export power limit set", + "", + data, + ) + self.assertSensor("house_consumption", 2488, "W", data) + self.assertSensor("battery_soc", 99, "%", data) + self.assertSensor("battery_temperature", 34.0, "C", data) + + # Both MPPT channels present (745 LV has 2 MPPTs) + self.assertSensor("pmppt1", 1369, "W", data) + self.assertSensor("pmppt2", 1127, "W", data) + self.assertSensor("imppt1", 5.5, "A", data) + self.assertSensor("imppt2", 4.3, "A", data) + + # L2 and L3 grid sensors must be absent on this single-phase inverter + self.assertNotIn("vgrid2", data) + self.assertNotIn("vgrid3", data) + self.assertNotIn("igrid2", data) + self.assertNotIn("igrid3", data) + self.assertNotIn("backup_v2", data) + self.assertNotIn("backup_v3", data) + + # Ghost sensors from larger inverters must be absent + self.assertNotIn("vpv5", data) + self.assertNotIn("vpv16", data) + self.assertNotIn("pmppt3", data) + self.assertNotIn("imppt3", data) + + class GW20K_ET_Test(EtMock): def __init__(self, methodName="runTest"):