Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,5 @@ dmypy.json
# Version file needed for CI publishing
VERSION

# Local MCP server config (contains credentials)
.mcp.json
38 changes: 34 additions & 4 deletions goodwe/et.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *

Expand Down Expand Up @@ -763,16 +763,29 @@ 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
self._sensors = tuple(filter(self._single_phase_only, self._sensors))
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
Expand All @@ -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))
Expand Down
5 changes: 5 additions & 0 deletions goodwe/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"EMN",
"ERN",
"EBN",
"ESC",
"HLB",
"HMB",
"HBB",
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/sample/et/GW8000M-ES-C10_battery_info.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f7033000000100000101540069006900000063006400010000000c000000000000000000000000000000000000000000000000f5f2
1 change: 1 addition & 0 deletions tests/sample/et/GW8000M-ES-C10_device_info.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f703420079226000003538303030455343323534533033363747573630303045533230000500050521000f01ed30343037302d30352d53313330323132302d31352d5330313131
1 change: 1 addition & 0 deletions tests/sample/et/GW8000M-ES-C10_meter_data.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f703fa00070000000a00010001002f00000000002f008b008203e703e7008213a300003c87000000000000002f00000000000000000000002f0000008b00000000000000000000008b0000010700000000000000000000010700ff00000000000000000000000000000000092800000000000b000000000000ffffffffffffffffffffffff000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000101800000000000000000000000000000000000000000000101800000000000349ae0000000000000000000000000000000000000000000349ae1a06ec3f
1 change: 1 addition & 0 deletions tests/sample/et/GW8000M-ES-C10_mppt_data.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f7037a000009c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffffffff05590467ffffffffffffffffffffffff0037002bffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5803
1 change: 1 addition & 0 deletions tests/sample/et/GW8000M-ES-C10_running_data.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aa55f703fa1a06150e051509ad00370000055809ad00000000046609f0002b000000000000000000000000000002020939006313a30000090500000000000000000000000000000000000000000001000009050000002900000009000008ee0929006113a30001000008c5000000000000000000000000000000000000000000000000000000170000000000000000000008c500000009000001df000001e28880120d120f02360006000000230003000000cd00010201000000000000c61f000000d70000aba500001dd200ae0000018500080000fb3d00e700002a3c002800002a25000a0000000000010000000000000000000001100100000200660000c72b
152 changes: 152 additions & 0 deletions tests/test_et.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down