goodwe 0.3.6__tar.gz → 0.4.0__tar.gz

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.
Files changed (30) hide show
  1. {goodwe-0.3.6/goodwe.egg-info → goodwe-0.4.0}/PKG-INFO +12 -7
  2. {goodwe-0.3.6 → goodwe-0.4.0}/README.md +11 -6
  3. goodwe-0.4.0/VERSION +1 -0
  4. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/__init__.py +32 -37
  5. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/const.py +1 -0
  6. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/dt.py +8 -12
  7. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/es.py +14 -14
  8. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/et.py +33 -67
  9. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/inverter.py +35 -38
  10. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/modbus.py +106 -6
  11. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/model.py +0 -4
  12. goodwe-0.4.0/goodwe/protocol.py +618 -0
  13. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/sensor.py +7 -23
  14. {goodwe-0.3.6 → goodwe-0.4.0/goodwe.egg-info}/PKG-INFO +12 -7
  15. {goodwe-0.3.6 → goodwe-0.4.0}/tests/test_dt.py +3 -3
  16. {goodwe-0.3.6 → goodwe-0.4.0}/tests/test_es.py +2 -16
  17. {goodwe-0.3.6 → goodwe-0.4.0}/tests/test_et.py +59 -67
  18. goodwe-0.4.0/tests/test_modbus.py +111 -0
  19. {goodwe-0.3.6 → goodwe-0.4.0}/tests/test_protocol.py +42 -33
  20. {goodwe-0.3.6 → goodwe-0.4.0}/tests/test_sensor.py +0 -10
  21. goodwe-0.3.6/VERSION +0 -1
  22. goodwe-0.3.6/goodwe/protocol.py +0 -338
  23. goodwe-0.3.6/tests/test_modbus.py +0 -66
  24. {goodwe-0.3.6 → goodwe-0.4.0}/LICENSE +0 -0
  25. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe/exceptions.py +0 -0
  26. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe.egg-info/SOURCES.txt +0 -0
  27. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe.egg-info/dependency_links.txt +0 -0
  28. {goodwe-0.3.6 → goodwe-0.4.0}/goodwe.egg-info/top_level.txt +0 -0
  29. {goodwe-0.3.6 → goodwe-0.4.0}/pyproject.toml +0 -0
  30. {goodwe-0.3.6 → goodwe-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.3.6
3
+ Version: 0.4.0
4
4
  Summary: Read data from GoodWe inverter via local network
5
5
  Home-page: https://github.com/marcelblijleven/goodwe
6
6
  Author: Martin Letenay, Marcel Blijleven
@@ -32,15 +32,21 @@ License-File: LICENSE
32
32
  Library for connecting to GoodWe inverter over local network and retrieving runtime sensor values and configuration
33
33
  parameters.
34
34
 
35
- It has been reported to work on GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS, and XS families of inverters. It may
36
- work on other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported communication
37
- protocols.
35
+ It has been reported to work with GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS, and XS families of inverters. It
36
+ should work with other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported
37
+ communication protocols.
38
+ In general, if you can connect to your inverter with the official mobile app (SolarGo/PvMaster) over Wi-Fi (not
39
+ bluetooth), this library should work.
38
40
 
39
41
  (If you can't communicate with the inverter despite your model is listed above, it is possible you have old ARM firmware
40
42
  version. You should ask manufacturer support to upgrade your ARM firmware (not just inverter firmware) to be able to
41
- communicate with the inveter via UDP.)
43
+ communicate with the inverter via UDP.)
42
44
 
43
- White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models.
45
+ White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models are
46
+ know to work properly.
47
+
48
+ Since v0.4.x the library also supports standard Modbus/TCP over port 502.
49
+ This protocol is supported by the V2.0 version of LAN+WiFi communication dongle (model WLA0000-01-00P).
44
50
 
45
51
  ## Usage
46
52
 
@@ -72,4 +78,3 @@ asyncio.run(get_runtime_data())
72
78
  - https://github.com/mletenay/home-assistant-goodwe-inverter
73
79
  - https://github.com/yasko-pv/modbus-log
74
80
  - https://github.com/tkubec/GoodWe
75
- - https://github.com/OpenEMS/openems
@@ -8,15 +8,21 @@
8
8
  Library for connecting to GoodWe inverter over local network and retrieving runtime sensor values and configuration
9
9
  parameters.
10
10
 
11
- It has been reported to work on GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS, and XS families of inverters. It may
12
- work on other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported communication
13
- protocols.
11
+ It has been reported to work with GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS, and XS families of inverters. It
12
+ should work with other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported
13
+ communication protocols.
14
+ In general, if you can connect to your inverter with the official mobile app (SolarGo/PvMaster) over Wi-Fi (not
15
+ bluetooth), this library should work.
14
16
 
15
17
  (If you can't communicate with the inverter despite your model is listed above, it is possible you have old ARM firmware
16
18
  version. You should ask manufacturer support to upgrade your ARM firmware (not just inverter firmware) to be able to
17
- communicate with the inveter via UDP.)
19
+ communicate with the inverter via UDP.)
18
20
 
19
- White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models.
21
+ White-label (GoodWe manufactured) inverters may work as well, e.g. General Electric GEP (PSB, PSC) and GEH models are
22
+ know to work properly.
23
+
24
+ Since v0.4.x the library also supports standard Modbus/TCP over port 502.
25
+ This protocol is supported by the V2.0 version of LAN+WiFi communication dongle (model WLA0000-01-00P).
20
26
 
21
27
  ## Usage
22
28
 
@@ -48,4 +54,3 @@ asyncio.run(get_runtime_data())
48
54
  - https://github.com/mletenay/home-assistant-goodwe-inverter
49
55
  - https://github.com/yasko-pv/modbus-log
50
56
  - https://github.com/tkubec/GoodWe
51
- - https://github.com/OpenEMS/openems
goodwe-0.4.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -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()
@@ -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] = {
@@ -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 = {}
@@ -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)
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import Tuple
5
5
 
6
- from .exceptions import RequestFailedException, RequestRejectedException
6
+ from .exceptions import RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
10
10
  from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
11
- from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand
11
+ from .protocol import ProtocolCommand
12
12
  from .sensor import *
13
13
 
14
14
  logger = logging.getLogger(__name__)
@@ -152,6 +152,10 @@ class ET(Inverter):
152
152
  read_bytes4_signed(data, 35182) -
153
153
  read_bytes2_signed(data, 35140),
154
154
  "House Consumption", "W", Kind.AC),
155
+
156
+ # Power4S("pbattery2", 35264, "Battery2 Power", Kind.BAT),
157
+ # Integer("battery2_mode", 35266, "Battery2 Mode code", "", Kind.BAT),
158
+ # Enum2("battery2_mode_label", 35184, BATTERY_MODES, "Battery2 Mode", Kind.BAT),
155
159
  )
156
160
 
157
161
  # Modbus registers from offset 0x9088 (37000)
@@ -252,8 +256,7 @@ class ET(Inverter):
252
256
  Apparent4("meter_apparent_power_total", 36041, "Meter Apparent Power Total", Kind.GRID),
253
257
  Integer("meter_type", 36043, "Meter Type", "", Kind.GRID), # (0: Single phase, 1: 3P3W, 2: 3P4W, 3: HomeKit)
254
258
  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
259
+ # Sensors added in some ARM fw update, read when flag _has_meter_extended is on
257
260
  Power4S("meter2_active_power", 36045, "Meter 2 Active Power", Kind.GRID),
258
261
  Float("meter2_e_total_exp", 36047, 1000, "Meter 2 Total Energy (export)", "kWh", Kind.GRID),
259
262
  Float("meter2_e_total_imp", 36049, 1000, "Meter 2 Total Energy (import)", "kWh", Kind.GRID),
@@ -264,15 +267,6 @@ class ET(Inverter):
264
267
  Current("meter_current1", 36055, "Meter L1 Current", Kind.GRID),
265
268
  Current("meter_current2", 36056, "Meter L2 Current", Kind.GRID),
266
269
  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
270
  )
277
271
 
278
272
  # Inverter's MPPT data
@@ -415,25 +409,23 @@ class ET(Inverter):
415
409
  Integer("eco_mode_enable", 47612, "Eco Mode Switch"),
416
410
  )
417
411
 
418
- def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
419
- super().__init__(host, comm_addr, timeout, retries)
412
+ def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
413
+ super().__init__(host, port, comm_addr, timeout, retries)
420
414
  if not self.comm_addr:
421
415
  # Set the default inverter address
422
416
  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)
417
+ self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x88b8, 0x0021)
418
+ self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x891c, 0x007d)
419
+ self._READ_METER_DATA: ProtocolCommand = self._read_command(0x8ca0, 0x2d)
420
+ self._READ_METER_DATA_EXTENDED: ProtocolCommand = self._read_command(0x8ca0, 0x3a)
421
+ self._READ_BATTERY_INFO: ProtocolCommand = self._read_command(0x9088, 0x0018)
422
+ self._READ_BATTERY2_INFO: ProtocolCommand = self._read_command(0x9858, 0x0016)
423
+ self._READ_MPPT_DATA: ProtocolCommand = self._read_command(0x89e5, 0x3d)
431
424
  self._has_eco_mode_v2: bool = True
432
425
  self._has_peak_shaving: bool = True
433
426
  self._has_battery: bool = True
434
427
  self._has_battery2: bool = False
435
428
  self._has_meter_extended: bool = False
436
- self._has_meter_extended2: bool = False
437
429
  self._has_mppt: bool = False
438
430
  self._sensors = self.__all_sensors
439
431
  self._sensors_battery = self.__all_sensors_battery
@@ -452,11 +444,6 @@ class ET(Inverter):
452
444
  """Filter to exclude extended meter sensors"""
453
445
  return s.offset < 36045
454
446
 
455
- @staticmethod
456
- def _not_extended_meter2(s: Sensor) -> bool:
457
- """Filter to exclude extended meter sensors"""
458
- return s.offset < 36058
459
-
460
447
  async def read_device_info(self):
461
448
  response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
462
449
  response = response.response_data()
@@ -487,36 +474,29 @@ class ET(Inverter):
487
474
  if is_2_battery(self) or self.rated_power >= 25000:
488
475
  self._has_battery2 = True
489
476
 
490
- if is_745_platform(self) or self.rated_power >= 15000:
477
+ if self.rated_power >= 15000:
491
478
  self._has_mppt = True
492
479
  self._has_meter_extended = True
493
- self._has_meter_extended2 = True
494
480
  else:
495
481
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
496
482
 
497
483
  # Check and add EcoModeV2 settings added in (ETU fw 19)
498
484
  try:
499
- await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47547, 6))
485
+ await self._read_from_socket(self._read_command(47547, 6))
500
486
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
501
487
  except RequestRejectedException as ex:
502
488
  if ex.message == 'ILLEGAL DATA ADDRESS':
503
- logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
489
+ logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
504
490
  self._has_eco_mode_v2 = False
505
- except RequestFailedException as ex:
506
- logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
507
- self._has_eco_mode_v2 = False
508
491
 
509
492
  # Check and add Peak Shaving settings added in (ETU fw 22)
510
493
  try:
511
- await self._read_from_socket(ModbusReadCommand(self.comm_addr, 47589, 6))
494
+ await self._read_from_socket(self._read_command(47589, 6))
512
495
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
513
496
  except RequestRejectedException as ex:
514
497
  if ex.message == 'ILLEGAL DATA ADDRESS':
515
- logger.debug("PeakShaving setting not supported, disabling it.")
498
+ logger.debug("Cannot read PeakShaving setting, disabling it.")
516
499
  self._has_peak_shaving = False
517
- except RequestFailedException as ex:
518
- logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
519
- self._has_peak_shaving = False
520
500
 
521
501
  async def read_runtime_data(self) -> Dict[str, Any]:
522
502
  response = await self._read_from_socket(self._READ_RUNNING_DATA)
@@ -529,7 +509,7 @@ class ET(Inverter):
529
509
  data.update(self._map_response(response, self._sensors_battery))
530
510
  except RequestRejectedException as ex:
531
511
  if ex.message == 'ILLEGAL DATA ADDRESS':
532
- logger.warning("Battery values not supported, disabling further attempts.")
512
+ logger.warning("Cannot read battery values, disabling further attempts.")
533
513
  self._has_battery = False
534
514
  else:
535
515
  raise ex
@@ -540,32 +520,18 @@ class ET(Inverter):
540
520
  self._map_response(response, self._sensors_battery2))
541
521
  except RequestRejectedException as ex:
542
522
  if ex.message == 'ILLEGAL DATA ADDRESS':
543
- logger.warning("Battery 2 values not supported, disabling further attempts.")
523
+ logger.warning("Cannot read battery 2 values, disabling further attempts.")
544
524
  self._has_battery2 = False
545
525
  else:
546
526
  raise ex
547
527
 
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:
528
+ if self._has_meter_extended:
563
529
  try:
564
530
  response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
565
531
  data.update(self._map_response(response, self._sensors_meter))
566
532
  except RequestRejectedException as ex:
567
533
  if ex.message == 'ILLEGAL DATA ADDRESS':
568
- logger.warning("Extended meter values not supported, disabling further attempts.")
534
+ logger.warning("Cannot read extended meter values, disabling further attempts.")
569
535
  self._has_meter_extended = False
570
536
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
571
537
  response = await self._read_from_socket(self._READ_METER_DATA)
@@ -583,7 +549,7 @@ class ET(Inverter):
583
549
  data.update(self._map_response(response, self._sensors_mppt))
584
550
  except RequestRejectedException as ex:
585
551
  if ex.message == 'ILLEGAL DATA ADDRESS':
586
- logger.warning("MPPT values not supported, disabling further attempts.")
552
+ logger.warning("Cannot read MPPT values, disabling further attempts.")
587
553
  self._has_mppt = False
588
554
  else:
589
555
  raise ex
@@ -598,7 +564,7 @@ class ET(Inverter):
598
564
 
599
565
  async def _read_setting(self, setting: Sensor) -> Any:
600
566
  count = (setting.size_ + (setting.size_ % 2)) // 2
601
- response = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count))
567
+ response = await self._read_from_socket(self._read_command(setting.offset, count))
602
568
  return setting.read_value(response)
603
569
 
604
570
  async def write_setting(self, setting_id: str, value: Any):
@@ -610,15 +576,15 @@ class ET(Inverter):
610
576
  async def _write_setting(self, setting: Sensor, value: Any):
611
577
  if setting.size_ == 1:
612
578
  # 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))
579
+ response = await self._read_from_socket(self._read_command(setting.offset, 1))
614
580
  raw_value = setting.encode_value(value, response.response_data()[0:2])
615
581
  else:
616
582
  raw_value = setting.encode_value(value)
617
583
  if len(raw_value) <= 2:
618
584
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
619
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value))
585
+ await self._read_from_socket(self._write_command(setting.offset, value))
620
586
  else:
621
- await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value))
587
+ await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
622
588
 
623
589
  async def read_settings_data(self) -> Dict[str, Any]:
624
590
  data = {}
@@ -649,7 +615,7 @@ class ET(Inverter):
649
615
  result.remove(OperationMode.ECO_DISCHARGE)
650
616
  return tuple(result)
651
617
 
652
- async def get_operation_mode(self) -> OperationMode:
618
+ async def get_operation_mode(self) -> OperationMode | None:
653
619
  mode_id = await self.read_setting('work_mode')
654
620
  try:
655
621
  mode = OperationMode(mode_id)
@@ -737,8 +703,8 @@ class ET(Inverter):
737
703
  return tuple(self._settings.values())
738
704
 
739
705
  async def _clear_battery_mode_param(self) -> None:
740
- await self._read_from_socket(ModbusWriteCommand(self.comm_addr, 0xb9ad, 1))
706
+ await self._read_from_socket(self._write_command(0xb9ad, 1))
741
707
 
742
708
  async def _set_offline(self, mode: bool) -> None:
743
709
  value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000')
744
- await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, 0xb997, value))
710
+ await self._read_from_socket(self._write_multi_command(0xb997, value))