goodwe 0.3.6__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
goodwe/__init__.py CHANGED
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Type
6
5
 
6
+ from .const import GOODWE_TCP_PORT, GOODWE_UDP_PORT
7
7
  from .dt import DT
8
8
  from .es import ES
9
9
  from .et import ET
@@ -26,8 +26,8 @@ DISCOVERY_COMMAND = Aa55ProtocolCommand("010200", "0182")
26
26
  _SUPPORTED_PROTOCOLS = [ET, DT, ES]
27
27
 
28
28
 
29
- async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: int = 1, retries: int = 3,
30
- do_discover: bool = True) -> Inverter:
29
+ async def connect(host: str, port: int = GOODWE_UDP_PORT, family: str = None, comm_addr: int = 0, timeout: int = 1,
30
+ retries: int = 3, do_discover: bool = True) -> Inverter:
31
31
  """Contact the inverter at the specified host/port and answer appropriate Inverter instance.
32
32
 
33
33
  The specific inverter family/type will be detected automatically, but it can be passed explicitly.
@@ -41,24 +41,24 @@ async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: in
41
41
 
42
42
  Raise InverterError if unable to contact or recognise supported inverter.
43
43
  """
44
- if family in ET_FAMILY:
45
- inv = ET(host, comm_addr, timeout, retries)
44
+ if family in ET_FAMILY or port == GOODWE_TCP_PORT:
45
+ inv = ET(host, port, comm_addr, timeout, retries)
46
46
  elif family in ES_FAMILY:
47
- inv = ES(host, comm_addr, timeout, retries)
47
+ inv = ES(host, port, comm_addr, timeout, retries)
48
48
  elif family in DT_FAMILY:
49
- inv = DT(host, comm_addr, timeout, retries)
49
+ inv = DT(host, port, comm_addr, timeout, retries)
50
50
  elif do_discover:
51
- return await discover(host, timeout, retries)
51
+ return await discover(host, port, timeout, retries)
52
52
  else:
53
53
  raise InverterError("Specify either an inverter family or set do_discover True")
54
54
 
55
- logger.debug("Connecting to %s family inverter at %s.", family, host)
55
+ logger.debug("Connecting to %s family inverter at %s:%s.", family, host, port)
56
56
  await inv.read_device_info()
57
57
  logger.debug("Connected to inverter %s, S/N:%s.", inv.model_name, inv.serial_number)
58
58
  return inv
59
59
 
60
60
 
61
- async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
61
+ async def discover(host: str, port: int = GOODWE_UDP_PORT, timeout: int = 1, retries: int = 3) -> Inverter:
62
62
  """Contact the inverter at the specified value and answer appropriate Inverter instance
63
63
 
64
64
  Raise InverterError if unable to contact or recognise supported inverter
@@ -67,28 +67,33 @@ async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
67
67
 
68
68
  # Try the common AA55C07F0102000241 command first and detect inverter type from serial_number
69
69
  try:
70
- logger.debug("Probing inverter at %s.", host)
71
- response = await DISCOVERY_COMMAND.execute(host, timeout, retries)
70
+ logger.debug("Probing inverter at %s:%s.", host, port)
71
+ response = await DISCOVERY_COMMAND.execute(UdpInverterProtocol(host, port, timeout, retries))
72
72
  response = response.response_data()
73
73
  model_name = response[5:15].decode("ascii").rstrip()
74
74
  serial_number = response[31:47].decode("ascii")
75
75
 
76
- inverter_class: Type[Inverter] | None = None
76
+ i: Inverter | None = None
77
77
  for model_tag in ET_MODEL_TAGS:
78
78
  if model_tag in serial_number:
79
79
  logger.debug("Detected ET/EH/BT/BH/GEH inverter %s, S/N:%s.", model_name, serial_number)
80
- inverter_class = ET
81
- for model_tag in ES_MODEL_TAGS:
82
- if model_tag in serial_number:
83
- logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number)
84
- inverter_class = ES
85
- for model_tag in DT_MODEL_TAGS:
86
- if model_tag in serial_number:
87
- logger.debug("Detected DT/MS/D-NS/XS/GEP inverter %s, S/N:%s.", model_name, serial_number)
88
- inverter_class = DT
89
- if inverter_class:
90
- i = inverter_class(host, 0, timeout, retries)
80
+ i = ET(host, port, 0, timeout, retries)
81
+ break
82
+ if not i:
83
+ for model_tag in ES_MODEL_TAGS:
84
+ if model_tag in serial_number:
85
+ logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number)
86
+ i = ES(host, port, 0, timeout, retries)
87
+ break
88
+ if not i:
89
+ for model_tag in DT_MODEL_TAGS:
90
+ if model_tag in serial_number:
91
+ logger.debug("Detected DT/MS/D-NS/XS/GEP inverter %s, S/N:%s.", model_name, serial_number)
92
+ i = DT(host, port, 0, timeout, retries)
93
+ break
94
+ if i:
91
95
  await i.read_device_info()
96
+ logger.debug("Connected to inverter %s, S/N:%s.", i.model_name, i.serial_number)
92
97
  return i
93
98
 
94
99
  except InverterError as ex:
@@ -96,7 +101,7 @@ async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter:
96
101
 
97
102
  # Probe inverter specific protocols
98
103
  for inv in _SUPPORTED_PROTOCOLS:
99
- i = inv(host, 0, timeout, retries)
104
+ i = inv(host, port, 0, timeout, retries)
100
105
  try:
101
106
  logger.debug("Probing %s inverter at %s.", inv.__name__, host)
102
107
  await i.read_device_info()
@@ -119,22 +124,12 @@ async def search_inverters() -> bytes:
119
124
  Raise InverterError if unable to contact any inverter
120
125
  """
121
126
  logger.debug("Searching inverters by broadcast to port 48899")
122
- loop = asyncio.get_running_loop()
123
127
  command = ProtocolCommand("WIFIKIT-214028-READ".encode("utf-8"), lambda r: True)
124
- response_future = loop.create_future()
125
- transport, _ = await loop.create_datagram_endpoint(
126
- lambda: UdpInverterProtocol(response_future, command, 1, 3),
127
- remote_addr=("255.255.255.255", 48899),
128
- allow_broadcast=True,
129
- )
130
128
  try:
131
- await response_future
132
- result = response_future.result()
129
+ result = await command.execute(UdpInverterProtocol("255.255.255.255", 48899, 1, 0))
133
130
  if result is not None:
134
- return result
131
+ return result.response_data()
135
132
  else:
136
133
  raise InverterError("No response received to broadcast request.")
137
134
  except asyncio.CancelledError:
138
135
  raise InverterError("No valid response received to broadcast request.") from None
139
- finally:
140
- transport.close()
goodwe/const.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from typing import Dict
2
2
 
3
+ GOODWE_TCP_PORT = 502
3
4
  GOODWE_UDP_PORT = 8899
4
5
 
5
6
  BATTERY_MODES: Dict[int, str] = {
goodwe/dt.py CHANGED
@@ -7,7 +7,7 @@ from .inverter import Inverter
7
7
  from .inverter import OperationMode
8
8
  from .inverter import SensorKind as Kind
9
9
  from .model import is_3_mppt, is_single_phase
10
- from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
10
+ from .protocol import ProtocolCommand
11
11
  from .sensor import *
12
12
 
13
13
 
@@ -110,10 +110,6 @@ class DT(Inverter):
110
110
  Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV),
111
111
  Integer("grid_export", 40327, "Grid Export Enabled", "", Kind.GRID),
112
112
  Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID),
113
- Integer("start", 40330, "Start / Power On", "", Kind.GRID),
114
- Integer("stop", 40331, "Stop / Power Off", "", Kind.GRID),
115
- Integer("restart", 40332, "Restart", "", Kind.GRID),
116
- Integer("grid_export_hw", 40345, "Grid Export Enabled (HW)", "", Kind.GRID),
117
113
  )
118
114
 
119
115
  # Settings for single phase inverters
@@ -126,13 +122,13 @@ class DT(Inverter):
126
122
  Integer("grid_export_limit", 40336, "Grid Export Limit", "%", Kind.GRID),
127
123
  )
128
124
 
129
- def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
130
- super().__init__(host, comm_addr, timeout, retries)
125
+ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
126
+ super().__init__(host, port, comm_addr, timeout, retries)
131
127
  if not self.comm_addr:
132
128
  # Set the default inverter address
133
129
  self.comm_addr = 0x7f
134
- self._READ_DEVICE_VERSION_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x7531, 0x0028)
135
- self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x7594, 0x0049)
130
+ self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
131
+ self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
136
132
  self._sensors = self.__all_sensors
137
133
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
138
134
 
@@ -184,7 +180,7 @@ class DT(Inverter):
184
180
  if not setting:
185
181
  raise ValueError(f'Unknown setting "{setting_id}"')
186
182
  count = (setting.size_ + (setting.size_ % 2)) // 2
187
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
183
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
188
184
  return setting.read_value(response)
189
185
 
190
186
  async def write_setting(self, setting_id: str, value: Any):
@@ -194,9 +190,9 @@ class DT(Inverter):
194
190
  raw_value = setting.encode_value(value)
195
191
  if len(raw_value) <= 2:
196
192
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
197
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
193
+ await self._read_from_socket(self._write_command(setting.offset, value))
198
194
  else:
199
- await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
195
+ await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
200
196
 
201
197
  async def read_settings_data(self) -> Dict[str, Any]:
202
198
  data = {}
goodwe/es.py CHANGED
@@ -7,8 +7,7 @@ from .exceptions import InverterError
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
10
- from .protocol import ProtocolCommand, Aa55ProtocolCommand, Aa55ReadCommand, Aa55WriteCommand, Aa55WriteMultiCommand, \
11
- ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
10
+ from .protocol import ProtocolCommand, Aa55ProtocolCommand, Aa55ReadCommand, Aa55WriteCommand, Aa55WriteMultiCommand
12
11
  from .sensor import *
13
12
 
14
13
  logger = logging.getLogger(__name__)
@@ -168,8 +167,8 @@ class ES(Inverter):
168
167
  ByteH("eco_mode_4_switch", 47567, "Eco Mode Group 4 Switch"),
169
168
  )
170
169
 
171
- def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
172
- super().__init__(host, comm_addr, timeout, retries)
170
+ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
171
+ super().__init__(host, port, comm_addr, timeout, retries)
173
172
  if not self.comm_addr:
174
173
  # Set the default inverter address
175
174
  self.comm_addr = 0xf7
@@ -178,11 +177,11 @@ class ES(Inverter):
178
177
  def _supports_eco_mode_v2(self) -> bool:
179
178
  if self.arm_version < 14:
180
179
  return False
181
- if "EMU" in self.serial_number or "EMJ" in self.serial_number:
180
+ if "EMU" in self.serial_number:
182
181
  return self.dsp1_version >= 11
183
- if "ESU" in self.serial_number or "ESA" in self.serial_number:
182
+ if "ESU" in self.serial_number:
184
183
  return self.dsp1_version >= 22
185
- if "BPS" in self.serial_number or "BPU" in self.serial_number:
184
+ if "BPS" in self.serial_number:
186
185
  return self.dsp1_version >= 10
187
186
  return False
188
187
 
@@ -192,7 +191,7 @@ class ES(Inverter):
192
191
  self.firmware = self._decode(response[0:5]).rstrip()
193
192
  self.model_name = self._decode(response[5:15]).rstrip()
194
193
  self.serial_number = self._decode(response[31:47])
195
- self.arm_firmware = self._decode(response[51:63]) # AKA software_version
194
+ self.software_version = self._decode(response[51:63])
196
195
  try:
197
196
  if len(self.firmware) >= 2:
198
197
  self.dsp1_version = int(self.firmware[0:2])
@@ -228,7 +227,7 @@ class ES(Inverter):
228
227
  async def _read_setting(self, setting: Sensor) -> Any:
229
228
  count = (setting.size_ + (setting.size_ % 2)) // 2
230
229
  if self._is_modbus_setting(setting):
231
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
230
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
232
231
  return setting.read_value(response)
233
232
  else:
234
233
  response = await self._read_from_socket(Aa55ReadCommand(setting.offset, count))
@@ -249,21 +248,22 @@ class ES(Inverter):
249
248
  if setting.size_ == 1:
250
249
  # modbus can address/store only 16 bit values, read the other 8 bytes
251
250
  if self._is_modbus_setting(setting):
252
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
251
+ response = await self._read_from_socket(self._read_command(setting.offset, 1))
252
+ raw_value = setting.encode_value(value, response.response_data()[0:2])
253
253
  else:
254
254
  response = await self._read_from_socket(Aa55ReadCommand(setting.offset, 1))
255
- raw_value = setting.encode_value(value, response.response_data()[0:2])
255
+ raw_value = setting.encode_value(value, response.response_data()[2:4])
256
256
  else:
257
257
  raw_value = setting.encode_value(value)
258
258
  if len(raw_value) <= 2:
259
259
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
260
260
  if self._is_modbus_setting(setting):
261
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
261
+ await self._read_from_socket(self._write_command(setting.offset, value))
262
262
  else:
263
263
  await self._read_from_socket(Aa55WriteCommand(setting.offset, value))
264
264
  else:
265
265
  if self._is_modbus_setting(setting):
266
- await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
266
+ await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
267
267
  else:
268
268
  await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value))
269
269
 
@@ -290,7 +290,7 @@ class ES(Inverter):
290
290
  result.remove(OperationMode.ECO_DISCHARGE)
291
291
  return tuple(result)
292
292
 
293
- async def get_operation_mode(self) -> OperationMode:
293
+ async def get_operation_mode(self) -> OperationMode | None:
294
294
  mode_id = await self.read_setting('work_mode')
295
295
  try:
296
296
  mode = OperationMode(mode_id)
goodwe/et.py CHANGED
@@ -7,8 +7,9 @@ from .exceptions import RequestFailedException, RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
10
+ from .modbus import ILLEGAL_DATA_ADDRESS
10
11
  from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
11
- from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
12
+ from .protocol import ProtocolCommand
12
13
  from .sensor import *
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -152,6 +153,10 @@ class ET(Inverter):
152
153
  read_bytes4_signed(data, 35182) -
153
154
  read_bytes2_signed(data, 35140),
154
155
  "House Consumption", "W", Kind.AC),
156
+
157
+ # Power4S("pbattery2", 35264, "Battery2 Power", Kind.BAT),
158
+ # Integer("battery2_mode", 35266, "Battery2 Mode code", "", Kind.BAT),
159
+ # Enum2("battery2_mode_label", 35184, BATTERY_MODES, "Battery2 Mode", Kind.BAT),
155
160
  )
156
161
 
157
162
  # Modbus registers from offset 0x9088 (37000)
@@ -252,8 +257,7 @@ class ET(Inverter):
252
257
  Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
253
258
  Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
254
259
  Integer("meter_sw_version", 36044, "Meter Software Version", "", Kind.GRID),
255
-
256
- # Sensors added in some ARM fw update (or platform 745/753), read when flag _has_meter_extended is on
260
+ # Sensors added in some ARM fw update, read when flag _has_meter_extended is on
257
261
  Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
258
262
  Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
259
263
  Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
@@ -264,15 +268,6 @@ class ET(Inverter):
264
268
  Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
265
269
  Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
266
270
  Current("meter_current3", 36057, "Meter L3 Current", Kind.GRID),
267
-
268
- Energy8("meter_e_total_exp1", 36092, "Meter Total Energy (export) L1", Kind.GRID),
269
- Energy8("meter_e_total_exp2", 36096, "Meter Total Energy (export) L2", Kind.GRID),
270
- Energy8("meter_e_total_exp3", 36100, "Meter Total Energy (export) L3", Kind.GRID),
271
- Energy8("meter_e_total_exp", 36104, "Meter Total Energy (export)", Kind.GRID),
272
- Energy8("meter_e_total_imp1", 36108, "Meter Total Energy (import) L1", Kind.GRID),
273
- Energy8("meter_e_total_imp2", 36112, "Meter Total Energy (import) L2", Kind.GRID),
274
- Energy8("meter_e_total_imp3", 36116, "Meter Total Energy (import) L3", Kind.GRID),
275
- Energy8("meter_e_total_imp", 36120, "Meter Total Energy (import)", Kind.GRID),
276
271
  )
277
272
 
278
273
  # Inverter's MPPT data
@@ -337,7 +332,7 @@ class ET(Inverter):
337
332
  # Modbus registers of inverter settings, offsets are modbus register addresses
338
333
  __all_settings: Tuple[Sensor, ...] = (
339
334
  Integer("comm_address", 45127, "Communication Address", ""),
340
-
335
+ Integer("modbus_baud_rate", 45132, "Modbus Baud rate", ""),
341
336
  Timestamp("time", 45200, "Inverter time"),
342
337
 
343
338
  Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
@@ -377,6 +372,51 @@ class ET(Inverter):
377
372
  ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
378
373
  EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
379
374
  ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),
375
+
376
+ # Direct BMS communication for EMS Control
377
+ Integer("bms_version", 47900, "BMS Version"),
378
+ Integer("bms_bat_modules", 47901, "BMS Battery Modules"),
379
+ # Real time read from BMS
380
+ Voltage("bms_bat_charge_v_max", 47902, "BMS Battery Charge Voltage (max)", Kind.BMS),
381
+ Current("bms_bat_charge_i_max", 47903, "BMS Battery Charge Current (max)", Kind.BMS),
382
+ Voltage("bms_bat_discharge_v_min", 47904, "BMS min. Battery Discharge Voltage (min)", Kind.BMS),
383
+ Current("bms_bat_discharge_i_max", 47905, "BMS max. Battery Discharge Current (max)", Kind.BMS),
384
+ Voltage("bms_bat_voltage", 47906, "BMS Battery Voltage", Kind.BMS),
385
+ Current("bms_bat_current", 47907, "BMS Battery Current", Kind.BMS),
386
+ #
387
+ Integer("bms_bat_soc", 47908, "BMS Battery State of Charge", "%", Kind.BMS),
388
+ Integer("bms_bat_soh", 47909, "BMS Battery State of Health", "%", Kind.BMS),
389
+ Temp("bms_bat_temperature", 47910, "BMS Battery Temperature", Kind.BMS),
390
+ Long("bms_bat_warning-code", 47911, "BMS Battery Warning Code"),
391
+ # Reserved
392
+ Long("bms_bat_alarm-code", 47913, "BMS Battery Alarm Code"),
393
+ Integer("bms_status", 47915, "BMS Status"),
394
+ Integer("bms_comm_loss_disable", 47916, "BMS Communication Loss Disable"),
395
+ # RW settings of BMS voltage rate
396
+ Integer("bms_battery_string_rate_v", 47917, "BMS Battery String Rate Voltage"),
397
+
398
+ # Direct BMS communication for EMS Control
399
+ Integer("bms2_version", 47918, "BMS2 Version"),
400
+ Integer("bms2_bat_modules", 47919, "BMS2 Battery Modules"),
401
+ # Real time read from BMS
402
+ Voltage("bms2_bat_charge_v_max", 47920, "BMS2 Battery Charge Voltage (max)", Kind.BMS),
403
+ Current("bms2_bat_charge_i_max", 47921, "BMS2 Battery Charge Current (max)", Kind.BMS),
404
+ Voltage("bms2_bat_discharge_v_min", 47922, "BMS2 min. Battery Discharge Voltage (min)", Kind.BMS),
405
+ Current("bms2_bat_discharge_i_max", 47923, "BMS2 max. Battery Discharge Current (max)", Kind.BMS),
406
+ Voltage("bms2_bat_voltage", 47924, "BMS2 Battery Voltage", Kind.BMS),
407
+ Current("bms2_bat_current", 47925, "BMS2 Battery Current", Kind.BMS),
408
+ #
409
+ Integer("bms2_bat_soc", 47926, "BMS2 Battery State of Charge", "%", Kind.BMS),
410
+ Integer("bms2_bat_soh", 47927, "BMS2 Battery State of Health", "%", Kind.BMS),
411
+ Temp("bms2_bat_temperature", 47928, "BMS2 Battery Temperature", Kind.BMS),
412
+ Long("bms2_bat_warning-code", 47929, "BMS2 Battery Warning Code"),
413
+ # Reserved
414
+ Long("bms2_bat_alarm-code", 47931, "BMS2 Battery Alarm Code"),
415
+ Integer("bms2_status", 47933, "BMS2 Status"),
416
+ Integer("bms2_comm_loss_disable", 47934, "BMS2 Communication Loss Disable"),
417
+ # RW settings of BMS voltage rate
418
+ Integer("bms2_battery_string_rate_v", 47935, "BMS2 Battery String Rate Voltage"),
419
+
380
420
  )
381
421
 
382
422
  # Settings added in ARM firmware 19
@@ -395,6 +435,7 @@ class ET(Inverter):
395
435
  Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
396
436
  Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
397
437
  Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
438
+ Integer("hardware_feed_power", 47599, "Hardware Feed Power"),
398
439
 
399
440
  Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
400
441
  )
@@ -415,25 +456,23 @@ class ET(Inverter):
415
456
  Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
416
457
  )
417
458
 
418
- def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
419
- super().__init__(host, comm_addr, timeout, retries)
459
+ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
460
+ super().__init__(host, port, comm_addr, timeout, retries)
420
461
  if not self.comm_addr:
421
462
  # Set the default inverter address
422
463
  self.comm_addr = 0xf7
423
- self._READ_DEVICE_VERSION_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x88b8, 0x0021)
424
- self._READ_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x891c, 0x007d)
425
- self._READ_METER_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x2d)
426
- self._READ_METER_DATA_EXTENDED: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x3a)
427
- self._READ_METER_DATA_EXTENDED2: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x7d)
428
- self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018)
429
- self._READ_BATTERY2_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9858, 0x0016)
430
- self._READ_MPPT_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x89e5, 0x3d)
464
+ self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
465
+ self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
466
+ self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
467
+ self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
468
+ self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
469
+ self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
470
+ self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
431
471
  self._has_eco_mode_v2: bool = True
432
472
  self._has_peak_shaving: bool = True
433
473
  self._has_battery: bool = True
434
474
  self._has_battery2: bool = False
435
475
  self._has_meter_extended: bool = False
436
- self._has_meter_extended2: bool = False
437
476
  self._has_mppt: bool = False
438
477
  self._sensors = self.__all_sensors
439
478
  self._sensors_battery = self.__all_sensors_battery
@@ -452,27 +491,22 @@ class ET(Inverter):
452
491
  """Filter to exclude extended meter sensors"""
453
492
  return s.offset < 36045
454
493
 
455
- @staticmethod
456
- def _not_extended_meter2(s: Sensor) -> bool:
457
- """Filter to exclude extended meter sensors"""
458
- return s.offset < 36058
459
-
460
494
  async def read_device_info(self):
461
495
  response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
462
496
  response = response.response_data()
463
- # Modbus registers from offset (35000)
497
+ # Modbus registers from 35000 - 35032
464
498
  self.modbus_version = read_unsigned_int(response, 0)
465
499
  self.rated_power = read_unsigned_int(response, 2)
466
500
  self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
467
- self.serial_number = self._decode(response[6:22])
468
- self.model_name = self._decode(response[22:32])
469
- self.dsp1_version = read_unsigned_int(response, 32)
470
- self.dsp2_version = read_unsigned_int(response, 34)
471
- self.dsp_svn_version = read_unsigned_int(response, 36)
472
- self.arm_version = read_unsigned_int(response, 38)
473
- self.arm_svn_version = read_unsigned_int(response, 40)
474
- self.firmware = self._decode(response[42:54])
475
- self.arm_firmware = self._decode(response[54:66])
501
+ self.serial_number = self._decode(response[6:22]) # 35003 - 350010
502
+ self.model_name = self._decode(response[22:32]) # 35011 - 35015
503
+ self.dsp1_version = read_unsigned_int(response, 32) # 35016
504
+ self.dsp2_version = read_unsigned_int(response, 34) # 35017
505
+ self.dsp_svn_version = read_unsigned_int(response, 36) # 35018
506
+ self.arm_version = read_unsigned_int(response, 38) # 35019
507
+ self.arm_svn_version = read_unsigned_int(response, 40) # 35020
508
+ self.firmware = self._decode(response[42:54]) # 35021 - 35027
509
+ self.arm_firmware = self._decode(response[54:66]) # 35027 - 35032
476
510
 
477
511
  if not is_4_mppt(self) and self.rated_power < 15000:
478
512
  # This inverter does not have 4 MPPTs or PV strings
@@ -487,34 +521,33 @@ class ET(Inverter):
487
521
  if is_2_battery(self) or self.rated_power >= 25000:
488
522
  self._has_battery2 = True
489
523
 
490
- if is_745_platform(self) or self.rated_power >= 15000:
524
+ if self.rated_power >= 15000:
491
525
  self._has_mppt = True
492
526
  self._has_meter_extended = True
493
- self._has_meter_extended2 = True
494
527
  else:
495
528
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
496
529
 
497
530
  # Check and add EcoModeV2 settings added in (ETU fw 19)
498
531
  try:
499
- await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47547, 6))
532
+ await self._read_from_socket(self._read_command(47547, 6))
500
533
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
501
534
  except RequestRejectedException as ex:
502
- if ex.message == 'ILLEGAL DATA ADDRESS':
535
+ if ex.message == ILLEGAL_DATA_ADDRESS:
503
536
  logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
504
537
  self._has_eco_mode_v2 = False
505
- except RequestFailedException as ex:
538
+ except RequestFailedException:
506
539
  logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
507
540
  self._has_eco_mode_v2 = False
508
541
 
509
542
  # Check and add Peak Shaving settings added in (ETU fw 22)
510
543
  try:
511
- await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47589, 6))
544
+ await self._read_from_socket(self._read_command(47589, 6))
512
545
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
513
546
  except RequestRejectedException as ex:
514
- if ex.message == 'ILLEGAL DATA ADDRESS':
547
+ if ex.message == ILLEGAL_DATA_ADDRESS:
515
548
  logger.debug("PeakShaving setting not supported, disabling it.")
516
549
  self._has_peak_shaving = False
517
- except RequestFailedException as ex:
550
+ except RequestFailedException:
518
551
  logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
519
552
  self._has_peak_shaving = False
520
553
 
@@ -528,8 +561,8 @@ class ET(Inverter):
528
561
  response = await self._read_from_socket(self._READ_BATTERY_INFO)
529
562
  data.update(self._map_response(response, self._sensors_battery))
530
563
  except RequestRejectedException as ex:
531
- if ex.message == 'ILLEGAL DATA ADDRESS':
532
- logger.warning("Battery values not supported, disabling further attempts.")
564
+ if ex.message == ILLEGAL_DATA_ADDRESS:
565
+ logger.info("Battery values not supported, disabling further attempts.")
533
566
  self._has_battery = False
534
567
  else:
535
568
  raise ex
@@ -539,33 +572,19 @@ class ET(Inverter):
539
572
  data.update(
540
573
  self._map_response(response, self._sensors_battery2))
541
574
  except RequestRejectedException as ex:
542
- if ex.message == 'ILLEGAL DATA ADDRESS':
543
- logger.warning("Battery 2 values not supported, disabling further attempts.")
575
+ if ex.message == ILLEGAL_DATA_ADDRESS:
576
+ logger.info("Battery 2 values not supported, disabling further attempts.")
544
577
  self._has_battery2 = False
545
578
  else:
546
579
  raise ex
547
580
 
548
- if self._has_meter_extended2:
549
- try:
550
- response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED2)
551
- data.update(self._map_response(response, self._sensors_meter))
552
- except RequestRejectedException as ex:
553
- if ex.message == 'ILLEGAL DATA ADDRESS':
554
- logger.info("Extended meter values not supported, disabling further attempts.")
555
- self._has_meter_extended2 = False
556
- self._sensors_meter = tuple(filter(self._not_extended_meter2, self._sensors_meter))
557
- response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
558
- data.update(
559
- self._map_response(response, self._sensors_meter))
560
- else:
561
- raise ex
562
- elif self._has_meter_extended:
581
+ if self._has_meter_extended:
563
582
  try:
564
583
  response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
565
584
  data.update(self._map_response(response, self._sensors_meter))
566
585
  except RequestRejectedException as ex:
567
- if ex.message == 'ILLEGAL DATA ADDRESS':
568
- logger.warning("Extended meter values not supported, disabling further attempts.")
586
+ if ex.message == ILLEGAL_DATA_ADDRESS:
587
+ logger.info("Extended meter values not supported, disabling further attempts.")
569
588
  self._has_meter_extended = False
570
589
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
571
590
  response = await self._read_from_socket(self._READ_METER_DATA)
@@ -582,8 +601,8 @@ class ET(Inverter):
582
601
  response = await self._read_from_socket(self._READ_MPPT_DATA)
583
602
  data.update(self._map_response(response, self._sensors_mppt))
584
603
  except RequestRejectedException as ex:
585
- if ex.message == 'ILLEGAL DATA ADDRESS':
586
- logger.warning("MPPT values not supported, disabling further attempts.")
604
+ if ex.message == ILLEGAL_DATA_ADDRESS:
605
+ logger.info("MPPT values not supported, disabling further attempts.")
587
606
  self._has_mppt = False
588
607
  else:
589
608
  raise ex
@@ -594,11 +613,17 @@ class ET(Inverter):
594
613
  setting = self._settings.get(setting_id)
595
614
  if not setting:
596
615
  raise ValueError(f'Unknown setting "{setting_id}"')
597
- return await self._read_setting(setting)
616
+ try:
617
+ return await self._read_setting(setting)
618
+ except RequestRejectedException as ex:
619
+ if ex.message == ILLEGAL_DATA_ADDRESS:
620
+ logger.debug("Unsupported setting %s", setting.id_)
621
+ self._settings.pop(setting_id, None)
622
+ return None
598
623
 
599
624
  async def _read_setting(self, setting: Sensor) -> Any:
600
625
  count = (setting.size_ + (setting.size_ % 2)) // 2
601
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
626
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
602
627
  return setting.read_value(response)
603
628
 
604
629
  async def write_setting(self, setting_id: str, value: Any):
@@ -610,15 +635,15 @@ class ET(Inverter):
610
635
  async def _write_setting(self, setting: Sensor, value: Any):
611
636
  if setting.size_ == 1:
612
637
  # modbus can address/store only 16 bit values, read the other 8 bytes
613
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1))
638
+ response = await self._read_from_socket(self._read_command(setting.offset, 1))
614
639
  raw_value = setting.encode_value(value, response.response_data()[0:2])
615
640
  else:
616
641
  raw_value = setting.encode_value(value)
617
642
  if len(raw_value) <= 2:
618
643
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
619
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
644
+ await self._read_from_socket(self._write_command(setting.offset, value))
620
645
  else:
621
- await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
646
+ await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
622
647
 
623
648
  async def read_settings_data(self) -> Dict[str, Any]:
624
649
  data = {}
@@ -626,7 +651,7 @@ class ET(Inverter):
626
651
  try:
627
652
  value = await self.read_setting(setting.id_)
628
653
  data[setting.id_] = value
629
- except ValueError:
654
+ except (ValueError, RequestFailedException):
630
655
  logger.exception("Error reading setting %s.", setting.id_)
631
656
  data[setting.id_] = None
632
657
  return data
@@ -649,7 +674,7 @@ class ET(Inverter):
649
674
  result.remove(OperationMode.ECO_DISCHARGE)
650
675
  return tuple(result)
651
676
 
652
- async def get_operation_mode(self) -> OperationMode:
677
+ async def get_operation_mode(self) -> OperationMode | None:
653
678
  mode_id = await self.read_setting('work_mode')
654
679
  try:
655
680
  mode = OperationMode(mode_id)
@@ -737,8 +762,8 @@ class ET(Inverter):
737
762
  return tuple(self._settings.values())
738
763
 
739
764
  async def _clear_battery_mode_param(self) -> None:
740
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, 0xb9ad, 1))
765
+ await self._read_from_socket(self._write_command(0xb9ad, 1))
741
766
 
742
767
  async def _set_offline(self, mode: bool) -> None:
743
768
  value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
744
- await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, 0xb997, value))
769
+ await self._read_from_socket(self._write_multi_command(0xb997, value))