Skip to content

Commit

Permalink
Add meter_e_total_exp (L1/L2/L3) to ETT/745 inverters
Browse files Browse the repository at this point in the history
  • Loading branch information
mletenay committed Jun 12, 2024
1 parent 40b3ba0 commit 8e1bda8
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 39 deletions.
38 changes: 35 additions & 3 deletions goodwe/et.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,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),
Expand All @@ -263,6 +264,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
Expand Down Expand Up @@ -414,6 +424,7 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int
self._READ_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x891c, 0x007d)
self._READ_METER_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x2d)
self._READ_METER_DATA_EXTENDED: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x3a)
self._READ_METER_DATA_EXTENDED2: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x7d)
self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018)
self._READ_BATTERY2_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9858, 0x0016)
self._READ_MPPT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89e5, 0x3d)
Expand All @@ -422,6 +433,7 @@ def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int
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
Expand All @@ -440,6 +452,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()
Expand Down Expand Up @@ -470,9 +487,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))

Expand Down Expand Up @@ -527,7 +545,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))
Expand Down
4 changes: 4 additions & 0 deletions goodwe/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
19 changes: 19 additions & 0 deletions goodwe/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down Expand Up @@ -816,6 +827,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:
Expand Down
55 changes: 19 additions & 36 deletions tests/test_et.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class EtMock(TestCase, ET):
def __init__(self, methodName='runTest'):
TestCase.__init__(self, methodName)
ET.__init__(self, "localhost")
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 = []

Expand All @@ -40,10 +40,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):
Expand Down Expand Up @@ -80,13 +81,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)
Expand Down Expand Up @@ -488,7 +491,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))
Expand Down Expand Up @@ -652,6 +655,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')
Expand All @@ -674,11 +678,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)
Expand Down Expand Up @@ -928,6 +935,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')
Expand All @@ -951,11 +959,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)
Expand Down Expand Up @@ -1098,32 +1107,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)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,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_timestamp(self):
testee = Timestamp("", 0, "", None)

Expand Down

0 comments on commit 8e1bda8

Please sign in to comment.