Skip to content

Commit

Permalink
Add BMS real time monitoring settings to ET
Browse files Browse the repository at this point in the history
  • Loading branch information
mletenay committed May 5, 2024
1 parent 750f3b3 commit e2b3068
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 29 deletions.
93 changes: 73 additions & 20 deletions goodwe/et.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import logging
from typing import Tuple

from .exceptions import RequestRejectedException
from .exceptions import RequestFailedException, RequestRejectedException
from .inverter import Inverter
from .inverter import OperationMode
from .inverter import 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 .protocol import ProtocolCommand
from .sensor import *
Expand Down Expand Up @@ -331,7 +332,7 @@ class ET(Inverter):
# Modbus registers of inverter settings, offsets are modbus register addresses
__all_settings: Tuple[Sensor, ...] = (
Integer("comm_address", 45127, "Communication Address", ""),

Integer("modbus_baud_rate", 45132, "Modbus Baud rate", ""),
Timestamp("time", 45200, "Inverter time"),

Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
Expand Down Expand Up @@ -371,6 +372,51 @@ class ET(Inverter):
ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),

# Direct BMS communication for EMS Control
Integer("bms_version", 47900, "BMS Version"),
Integer("bms_bat_modules", 47901, "BMS Battery Modules"),
# Real time read from BMS
Voltage("bms_bat_charge_v_max", 47902, "BMS Battery Charge Voltage (max)", Kind.BMS),
Current("bms_bat_charge_i_max", 47903, "BMS Battery Charge Current (max)", Kind.BMS),
Voltage("bms_bat_discharge_v_min", 47904, "BMS min. Battery Discharge Voltage (min)", Kind.BMS),
Current("bms_bat_discharge_i_max", 47905, "BMS max. Battery Discharge Current (max)", Kind.BMS),
Voltage("bms_bat_voltage", 47906, "BMS Battery Voltage", Kind.BMS),
Current("bms_bat_current", 47907, "BMS Battery Current", Kind.BMS),
#
Integer("bms_bat_soc", 47908, "BMS Battery State of Charge", "%", Kind.BMS),
Integer("bms_bat_soh", 47909, "BMS Battery State of Health", "%", Kind.BMS),
Temp("bms_bat_temperature", 47910, "BMS Battery Temperature", Kind.BMS),
Long("bms_bat_warning-code", 47911, "BMS Battery Warning Code"),
# Reserved
Long("bms_bat_alarm-code", 47913, "BMS Battery Alarm Code"),
Integer("bms_status", 47915, "BMS Status"),
Integer("bms_comm_loss_disable", 47916, "BMS Communication Loss Disable"),
# RW settings of BMS voltage rate
Integer("bms_battery_string_rate_v", 47917, "BMS Battery String Rate Voltage"),

# Direct BMS communication for EMS Control
Integer("bms2_version", 47918, "BMS2 Version"),
Integer("bms2_bat_modules", 47919, "BMS2 Battery Modules"),
# Real time read from BMS
Voltage("bms2_bat_charge_v_max", 47920, "BMS2 Battery Charge Voltage (max)", Kind.BMS),
Current("bms2_bat_charge_i_max", 47921, "BMS2 Battery Charge Current (max)", Kind.BMS),
Voltage("bms2_bat_discharge_v_min", 47922, "BMS2 min. Battery Discharge Voltage (min)", Kind.BMS),
Current("bms2_bat_discharge_i_max", 47923, "BMS2 max. Battery Discharge Current (max)", Kind.BMS),
Voltage("bms2_bat_voltage", 47924, "BMS2 Battery Voltage", Kind.BMS),
Current("bms2_bat_current", 47925, "BMS2 Battery Current", Kind.BMS),
#
Integer("bms2_bat_soc", 47926, "BMS2 Battery State of Charge", "%", Kind.BMS),
Integer("bms2_bat_soh", 47927, "BMS2 Battery State of Health", "%", Kind.BMS),
Temp("bms2_bat_temperature", 47928, "BMS2 Battery Temperature", Kind.BMS),
Long("bms2_bat_warning-code", 47929, "BMS2 Battery Warning Code"),
# Reserved
Long("bms2_bat_alarm-code", 47931, "BMS2 Battery Alarm Code"),
Integer("bms2_status", 47933, "BMS2 Status"),
Integer("bms2_comm_loss_disable", 47934, "BMS2 Communication Loss Disable"),
# RW settings of BMS voltage rate
Integer("bms2_battery_string_rate_v", 47935, "BMS2 Battery String Rate Voltage"),

)

# Settings added in ARM firmware 19
Expand All @@ -389,6 +435,7 @@ class ET(Inverter):
Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
Integer("hardware_feed_power", 47599, "Hardware Feed Power"),

Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
)
Expand Down Expand Up @@ -447,19 +494,19 @@ def _not_extended_meter(s: Sensor) -> bool:
async def read_device_info(self):
response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
response = response.response_data()
# Modbus registers from offset (35000)
# Modbus registers from 35000 - 35032
self.modbus_version = read_unsigned_int(response, 0)
self.rated_power = read_unsigned_int(response, 2)
self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
self.serial_number = self._decode(response[6:22])
self.model_name = self._decode(response[22:32])
self.dsp1_version = read_unsigned_int(response, 32)
self.dsp2_version = read_unsigned_int(response, 34)
self.dsp_svn_version = read_unsigned_int(response, 36)
self.arm_version = read_unsigned_int(response, 38)
self.arm_svn_version = read_unsigned_int(response, 40)
self.firmware = self._decode(response[42:54])
self.arm_firmware = self._decode(response[54:66])
self.serial_number = self._decode(response[6:22]) # 35003 - 350010
self.model_name = self._decode(response[22:32]) # 35011 - 35015
self.dsp1_version = read_unsigned_int(response, 32) # 35016
self.dsp2_version = read_unsigned_int(response, 34) # 35017
self.dsp_svn_version = read_unsigned_int(response, 36) # 35018
self.arm_version = read_unsigned_int(response, 38) # 35019
self.arm_svn_version = read_unsigned_int(response, 40) # 35020
self.firmware = self._decode(response[42:54]) # 35021 - 35027
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
Expand All @@ -485,7 +532,7 @@ async def read_device_info(self):
await self._read_from_socket(self._read_command(47547, 6))
self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
self._has_eco_mode_v2 = False

Expand All @@ -494,7 +541,7 @@ async def read_device_info(self):
await self._read_from_socket(self._read_command(47589, 6))
self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.debug("Cannot read PeakShaving setting, disabling it.")
self._has_peak_shaving = False

Expand All @@ -508,7 +555,7 @@ async def read_runtime_data(self) -> Dict[str, Any]:
response = await self._read_from_socket(self._READ_BATTERY_INFO)
data.update(self._map_response(response, self._sensors_battery))
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.warning("Cannot read battery values, disabling further attempts.")
self._has_battery = False
else:
Expand All @@ -519,7 +566,7 @@ async def read_runtime_data(self) -> Dict[str, Any]:
data.update(
self._map_response(response, self._sensors_battery2))
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.warning("Cannot read battery 2 values, disabling further attempts.")
self._has_battery2 = False
else:
Expand All @@ -530,7 +577,7 @@ async def read_runtime_data(self) -> Dict[str, Any]:
response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
data.update(self._map_response(response, self._sensors_meter))
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.warning("Cannot read extended meter values, disabling further attempts.")
self._has_meter_extended = False
self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
Expand All @@ -548,7 +595,7 @@ async def read_runtime_data(self) -> Dict[str, Any]:
response = await self._read_from_socket(self._READ_MPPT_DATA)
data.update(self._map_response(response, self._sensors_mppt))
except RequestRejectedException as ex:
if ex.message == 'ILLEGAL DATA ADDRESS':
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.warning("Cannot read MPPT values, disabling further attempts.")
self._has_mppt = False
else:
Expand All @@ -560,7 +607,13 @@ async def read_setting(self, setting_id: str) -> Any:
setting = self._settings.get(setting_id)
if not setting:
raise ValueError(f'Unknown setting "{setting_id}"')
return await self._read_setting(setting)
try:
return await self._read_setting(setting)
except RequestRejectedException as ex:
if ex.message == ILLEGAL_DATA_ADDRESS:
logger.debug("Unsupported setting %s", setting.id_)
self._settings.pop(setting_id, None)
return None

async def _read_setting(self, setting: Sensor) -> Any:
count = (setting.size_ + (setting.size_ % 2)) // 2
Expand Down Expand Up @@ -592,7 +645,7 @@ async def read_settings_data(self) -> Dict[str, Any]:
try:
value = await self.read_setting(setting.id_)
data[setting.id_] = value
except ValueError:
except (ValueError, RequestFailedException):
logger.exception("Error reading setting %s.", setting.id_)
data[setting.id_] = None
return data
Expand Down
2 changes: 2 additions & 0 deletions goodwe/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ class SensorKind(Enum):
UPS - inverter ups/eps/backup output (e.g. ac voltage of backup/off-grid connected output)
BAT - battery (e.g. dc voltage of connected battery pack)
GRID - power grid/smart meter (e.g. active power exported to grid)
BMS - BMS direct data (e.g. dc voltage of)
"""

PV = 1
AC = 2
UPS = 3
BAT = 4
GRID = 5
BMS = 6


@dataclass
Expand Down
4 changes: 3 additions & 1 deletion goodwe/modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
MODBUS_WRITE_CMD: int = 0x6
MODBUS_WRITE_MULTI_CMD: int = 0x10

ILLEGAL_DATA_ADDRESS = 'ILLEGAL DATA ADDRESS'

FAILURE_CODES = {
1: "ILLEGAL FUNCTION",
2: "ILLEGAL DATA ADDRESS",
2: ILLEGAL_DATA_ADDRESS,
3: "ILLEGAL DATA VALUE",
4: "SLAVE DEVICE FAILURE",
5: "ACKNOWLEDGE",
Expand Down
17 changes: 9 additions & 8 deletions tests/test_et.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from goodwe.et import ET
from goodwe.exceptions import RequestRejectedException, RequestFailedException
from goodwe.inverter import OperationMode
from goodwe.modbus import ILLEGAL_DATA_ADDRESS
from goodwe.protocol import ModbusRtuReadCommand, ProtocolCommand, ProtocolResponse


Expand All @@ -26,8 +27,8 @@ async def _read_from_socket(self, command: ProtocolCommand) -> ProtocolResponse:
root_dir = os.path.dirname(os.path.abspath(__file__))
filename = self._mock_responses.get(command)
if filename is not None:
if 'ILLEGAL DATA ADDRESS' == filename:
raise RequestRejectedException('ILLEGAL DATA ADDRESS')
if ILLEGAL_DATA_ADDRESS == filename:
raise RequestRejectedException(ILLEGAL_DATA_ADDRESS)
with open(root_dir + '/sample/et/' + filename, 'r') as f:
response = bytes.fromhex(f.read())
if not command.validator(response):
Expand Down Expand Up @@ -56,8 +57,8 @@ def __init__(self, methodName='runTest'):
self.mock_response(self._READ_RUNNING_DATA, 'GW10K-ET_running_data.hex')
self.mock_response(self._READ_METER_DATA, 'GW10K-ET_meter_data.hex')
self.mock_response(self._READ_BATTERY_INFO, 'GW10K-ET_battery_info.hex')
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47547, 6), 'ILLEGAL DATA ADDRESS')
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47589, 6), 'ILLEGAL DATA ADDRESS')
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47547, 6), ILLEGAL_DATA_ADDRESS)
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47589, 6), ILLEGAL_DATA_ADDRESS)
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47515, 4), 'eco_mode_v1.hex')

def test_GW10K_ET_device_info(self):
Expand Down Expand Up @@ -237,7 +238,7 @@ def test_GW10K_ET_runtime_data(self):
self.assertFalse(self.sensor_map, f"Some sensors were not tested {self.sensor_map}")

def test_GW10K_ET_setting(self):
self.assertEqual(32, len(self.settings()))
self.assertEqual(65, len(self.settings()))
settings = {s.id_: s for s in self.settings()}
self.assertEqual('Timestamp', type(settings.get("time")).__name__)
self.assertEqual('EcoModeV1', type(settings.get("eco_mode_1")).__name__)
Expand Down Expand Up @@ -311,7 +312,7 @@ def __init__(self, methodName='runTest'):
EtMock.__init__(self, methodName)
self.mock_response(self._READ_DEVICE_VERSION_INFO, 'GW10K-ET_device_info_fw819.hex')
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47547, 6), 'eco_mode_v2.hex')
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47589, 6), 'ILLEGAL DATA ADDRESS')
self.mock_response(ModbusRtuReadCommand(self.comm_addr, 47589, 6), ILLEGAL_DATA_ADDRESS)
asyncio.get_event_loop().run_until_complete(self.read_device_info())

def test_GW10K_ET_fw819_device_info(self):
Expand All @@ -329,7 +330,7 @@ def test_GW10K_ET_fw819_device_info(self):
self.assertEqual('02041-19-S00', self.arm_firmware)

def test_GW10K_ET_settings_fw819(self):
self.assertEqual(38, len(self.settings()))
self.assertEqual(72, len(self.settings()))
settings = {s.id_: s for s in self.settings()}
self.assertEqual('EcoModeV2', type(settings.get("eco_mode_1")).__name__)
self.assertEqual(None, settings.get("peak_shaving_mode"))
Expand Down Expand Up @@ -369,7 +370,7 @@ def test_GW10K_ET_fw1023_device_info(self):
self.assertEqual('02041-23-S00', self.arm_firmware)

def test_GW10K_ET_setting_fw1023(self):
self.assertEqual(46, len(self.settings()))
self.assertEqual(80, len(self.settings()))
settings = {s.id_: s for s in self.settings()}
self.assertEqual('PeakShavingMode', type(settings.get("peak_shaving_mode")).__name__)

Expand Down

0 comments on commit e2b3068

Please sign in to comment.